From fcfc76cd46b56ed7c2b7ad026f06a43a1c55388b Mon Sep 17 00:00:00 2001 From: Andrey Sokolov Date: Mon, 23 May 2022 18:06:32 +0200 Subject: [PATCH] MLP-6 Similarity Groups in Find Usages initial support Added support for java and kt usages KTIJ-21819 IDEA-294547 GitOrigin-RevId: 5bf947ecd97ba1ec106adbb9469307a51cc9508c --- java/java-impl/src/META-INF/JavaPlugin.xml | 2 + .../JavaSimilarityPropertiesExtractor.java | 237 +++++++++++++++++ .../JavaUsageSimilarityFeaturesProvider.java | 74 ++++++ .../testData/findSimilar/Anonymous.java | 12 + .../findSimilar/FeaturesProvider.java | 8 + .../testData/findSimilar/ForFeatures.java | 10 + .../testData/findSimilar/Increment.java | 9 + .../testData/findSimilar/Lambda.java | 12 + .../testData/findSimilar/MethodReference.java | 14 + .../usageGroups/arrayAccess/Market.java | 17 ++ .../usageGroups/arrayAccess/results.txt | 10 + .../binaryExpressions/UsageGroups.java | 14 + .../usageGroups/binaryExpressions/results.txt | 9 + .../usageGroups/mapUsages/MapUsages.java | 22 ++ .../usageGroups/mapUsages/results.txt | 10 + .../streamUsages/StreamUsages.java | 23 ++ .../usageGroups/streamUsages/results.txt | 12 + .../JavaFindUsagesGroupsTest.java | 69 +++++ .../JavaUsagesBySimilarityTest.java | 109 ++++++++ .../openapi/actionSystem/ActionPlaces.java | 3 +- .../messages/CodeInsightBundle.properties | 2 +- .../find/findUsages/FindUsagesManager.java | 8 +- .../src/messages/ActionsBundle.properties | 1 + .../src/META-INF/LangExtensionPoints.xml | 1 + .../src/META-INF/LangExtensions.xml | 1 + .../src/idea/LangActions.xml | 1 + .../similarity/LocationLinkComponent.java | 39 +++ .../MostCommonUsagePatternsComponent.java | 250 ++++++++++++++++++ .../similarity/SimilarUsagesComponent.java | 81 ++++++ .../similarity/SimilarUsagesContextPanel.java | 131 +++++++++ .../similarity/UsageCodeSnippetComponent.java | 85 ++++++ .../usages/impl/UsageNodeTreeBuilder.java | 4 + .../usages/impl/UsagePreviewPanel.java | 53 +++- .../intellij/usages/impl/UsageViewImpl.java | 2 +- .../resources/idea/UsageViewActions.xml | 12 + .../messages/UsageViewBundle.properties | 12 +- .../usages/UsageInfoToUsageConverter.java | 34 +++ .../intellij/usages/similarity/bag/Bag.java | 86 ++++++ .../bag/BagsDistanceCalculator.java | 29 ++ .../clustering/ClusteringSearchSession.java | 127 +++++++++ .../similarity/clustering/UsageCluster.java | 64 +++++ .../UsageSimilarityFeaturesProvider.java | 24 ++ .../UsageSimilarityFeaturesRecorder.java | 104 ++++++++ .../ShowUsageFeaturesInternalAction.java | 80 ++++++ ...larityReadWriteUsageInfo2UsageAdapter.java | 59 +++++ .../usageAdapter/SimilarityUsage.java | 44 +++ .../SimilarityUsageInfo2UsageAdapter.java | 56 ++++ .../KotlinSimilarityPropertiesExtractor.kt | 63 +++++ .../KotlinUsageSimilarityFeaturesProvider.kt | 46 ++++ .../KotlinGroupUsagesBySimilarityTest.kt | 64 +++++ .../findUsages/similarity/chainCalls.kt | 39 +++ .../similarity/chainCalls.results.txt | 12 + .../testData/findUsages/similarity/filter.kt | 12 + .../findUsages/similarity/filter.results.txt | 10 + .../testData/findUsages/similarity/general.kt | 22 ++ .../findUsages/similarity/general.results.txt | 8 + .../testData/findUsages/similarity/listAdd.kt | 8 + .../findUsages/similarity/listAdd.results.txt | 9 + .../findUsages/similarity/simpleFeatures.kt | 2 + .../plugin/resources/META-INF/kotlin-core.xml | 2 + 60 files changed, 2356 insertions(+), 7 deletions(-) create mode 100644 java/java-impl/src/com/intellij/find/findUsages/similarity/JavaSimilarityPropertiesExtractor.java create mode 100644 java/java-impl/src/com/intellij/find/findUsages/similarity/JavaUsageSimilarityFeaturesProvider.java create mode 100644 java/java-tests/testData/findSimilar/Anonymous.java create mode 100644 java/java-tests/testData/findSimilar/FeaturesProvider.java create mode 100644 java/java-tests/testData/findSimilar/ForFeatures.java create mode 100644 java/java-tests/testData/findSimilar/Increment.java create mode 100644 java/java-tests/testData/findSimilar/Lambda.java create mode 100644 java/java-tests/testData/findSimilar/MethodReference.java create mode 100644 java/java-tests/testData/findSimilar/usageGroups/arrayAccess/Market.java create mode 100644 java/java-tests/testData/findSimilar/usageGroups/arrayAccess/results.txt create mode 100644 java/java-tests/testData/findSimilar/usageGroups/binaryExpressions/UsageGroups.java create mode 100644 java/java-tests/testData/findSimilar/usageGroups/binaryExpressions/results.txt create mode 100644 java/java-tests/testData/findSimilar/usageGroups/mapUsages/MapUsages.java create mode 100644 java/java-tests/testData/findSimilar/usageGroups/mapUsages/results.txt create mode 100644 java/java-tests/testData/findSimilar/usageGroups/streamUsages/StreamUsages.java create mode 100644 java/java-tests/testData/findSimilar/usageGroups/streamUsages/results.txt create mode 100644 java/java-tests/testSrc/com/intellij/find/groupSimilar/JavaFindUsagesGroupsTest.java create mode 100644 java/java-tests/testSrc/com/intellij/find/groupSimilar/JavaUsagesBySimilarityTest.java create mode 100644 platform/usageView-impl/src/com/intellij/find/findUsages/similarity/LocationLinkComponent.java create mode 100644 platform/usageView-impl/src/com/intellij/find/findUsages/similarity/MostCommonUsagePatternsComponent.java create mode 100644 platform/usageView-impl/src/com/intellij/find/findUsages/similarity/SimilarUsagesComponent.java create mode 100644 platform/usageView-impl/src/com/intellij/find/findUsages/similarity/SimilarUsagesContextPanel.java create mode 100644 platform/usageView-impl/src/com/intellij/find/findUsages/similarity/UsageCodeSnippetComponent.java create mode 100644 platform/usageView/src/com/intellij/usages/similarity/bag/Bag.java create mode 100644 platform/usageView/src/com/intellij/usages/similarity/bag/BagsDistanceCalculator.java create mode 100644 platform/usageView/src/com/intellij/usages/similarity/clustering/ClusteringSearchSession.java create mode 100644 platform/usageView/src/com/intellij/usages/similarity/clustering/UsageCluster.java create mode 100644 platform/usageView/src/com/intellij/usages/similarity/features/UsageSimilarityFeaturesProvider.java create mode 100644 platform/usageView/src/com/intellij/usages/similarity/features/UsageSimilarityFeaturesRecorder.java create mode 100644 platform/usageView/src/com/intellij/usages/similarity/internal/ShowUsageFeaturesInternalAction.java create mode 100644 platform/usageView/src/com/intellij/usages/similarity/usageAdapter/SimilarityReadWriteUsageInfo2UsageAdapter.java create mode 100644 platform/usageView/src/com/intellij/usages/similarity/usageAdapter/SimilarityUsage.java create mode 100644 platform/usageView/src/com/intellij/usages/similarity/usageAdapter/SimilarityUsageInfo2UsageAdapter.java create mode 100644 plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/findUsages/similarity/KotlinSimilarityPropertiesExtractor.kt create mode 100644 plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/findUsages/similarity/KotlinUsageSimilarityFeaturesProvider.kt create mode 100644 plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/findUsages/KotlinGroupUsagesBySimilarityTest.kt create mode 100644 plugins/kotlin/idea/tests/testData/findUsages/similarity/chainCalls.kt create mode 100644 plugins/kotlin/idea/tests/testData/findUsages/similarity/chainCalls.results.txt create mode 100644 plugins/kotlin/idea/tests/testData/findUsages/similarity/filter.kt create mode 100644 plugins/kotlin/idea/tests/testData/findUsages/similarity/filter.results.txt create mode 100644 plugins/kotlin/idea/tests/testData/findUsages/similarity/general.kt create mode 100644 plugins/kotlin/idea/tests/testData/findUsages/similarity/general.results.txt create mode 100644 plugins/kotlin/idea/tests/testData/findUsages/similarity/listAdd.kt create mode 100644 plugins/kotlin/idea/tests/testData/findUsages/similarity/listAdd.results.txt create mode 100644 plugins/kotlin/idea/tests/testData/findUsages/similarity/simpleFeatures.kt diff --git a/java/java-impl/src/META-INF/JavaPlugin.xml b/java/java-impl/src/META-INF/JavaPlugin.xml index 6cbc67df204d..c4b7309c1fac 100644 --- a/java/java-impl/src/META-INF/JavaPlugin.xml +++ b/java/java-impl/src/META-INF/JavaPlugin.xml @@ -2274,6 +2274,8 @@ + + diff --git a/java/java-impl/src/com/intellij/find/findUsages/similarity/JavaSimilarityPropertiesExtractor.java b/java/java-impl/src/com/intellij/find/findUsages/similarity/JavaSimilarityPropertiesExtractor.java new file mode 100644 index 000000000000..875f1deb687a --- /dev/null +++ b/java/java-impl/src/com/intellij/find/findUsages/similarity/JavaSimilarityPropertiesExtractor.java @@ -0,0 +1,237 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.find.findUsages.similarity; + +import com.intellij.openapi.util.registry.Registry; +import com.intellij.psi.*; +import com.intellij.psi.tree.IElementType; +import com.intellij.usages.similarity.bag.Bag; +import com.intellij.usages.similarity.features.UsageSimilarityFeaturesRecorder; +import com.intellij.util.ObjectUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static com.intellij.psi.JavaTokenType.FINAL_KEYWORD; + +public class JavaSimilarityPropertiesExtractor extends JavaRecursiveElementVisitor { + private final @NotNull UsageSimilarityFeaturesRecorder myUsageSimilarityFeaturesRecorder; + private final @NotNull PsiElement myContext; + + public JavaSimilarityPropertiesExtractor(@NotNull PsiElement context) { + myUsageSimilarityFeaturesRecorder = new UsageSimilarityFeaturesRecorder(context); + myContext = context; + } + + public @NotNull Bag getFeatures() { + myContext.accept(this); + return myUsageSimilarityFeaturesRecorder.getFeatures(); + } + + @Override + public void visitKeyword(PsiKeyword keyword) { + final IElementType type = keyword.getTokenType(); + if (!type.equals(FINAL_KEYWORD)) { + myUsageSimilarityFeaturesRecorder.addFeature(type.toString()); + } + super.visitKeyword(keyword); + } + + + @Override + public void visitVariable(PsiVariable variable) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(variable, "VAR: " + getTypeRepresentation(variable.getType())); + super.visitVariable(variable); + } + + @Override + public void visitNewExpression(PsiNewExpression expression) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(expression, "NEW: " + getTypeRepresentation(expression)); + super.visitNewExpression(expression); + } + + @Override + public void visitForStatement(PsiForStatement statement) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(statement, "FOR"); + super.visitForStatement(statement); + } + + @Override + public void visitForeachStatement(PsiForeachStatement statement) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(statement, "FOREACH"); + super.visitForeachStatement(statement); + } + + @Override + public void visitDoWhileStatement(PsiDoWhileStatement statement) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(statement, "DO"); + super.visitDoWhileStatement(statement); + } + + @Override + public void visitIfStatement(PsiIfStatement statement) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(statement, "IF"); + super.visitIfStatement(statement); + } + + @Override + public void visitAssignmentExpression(PsiAssignmentExpression expression) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(expression, expression.getOperationSign().getText()); + super.visitAssignmentExpression(expression); + } + + @Override + public void visitInstanceOfExpression(PsiInstanceOfExpression expression) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(expression, "instanceof"); + super.visitInstanceOfExpression(expression); + } + + @Override + public void visitPolyadicExpression(PsiPolyadicExpression expression) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(expression, expression.getOperationTokenType().toString()); + super.visitPolyadicExpression(expression); + } + + @Override + public void visitSwitchStatement(PsiSwitchStatement statement) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(statement, "SWITCH"); + super.visitSwitchStatement(statement); + } + + @Override + public void visitWhileStatement(PsiWhileStatement statement) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(statement, "WHILE"); + super.visitWhileStatement(statement); + } + + @Override + public void visitReferenceExpression(PsiReferenceExpression expression) { + String tokenFeature = null; + if (!(expression instanceof PsiMethodReferenceExpression)) { + tokenFeature = "VAR: "; + if (!Registry.is("similarity.find.usages.fast.clustering")) { + tokenFeature += getTypeRepresentation(expression); + } + } + + if (tokenFeature != null) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(expression, tokenFeature); + } + super.visitReferenceExpression(expression); + } + + @Override + public void visitMethodReferenceExpression(PsiMethodReferenceExpression expression) { + String tokenFeature; + if (Registry.is("similarity.find.usages.fast.clustering")) { + final PsiElement referenceNameElement = expression.getReferenceNameElement(); + tokenFeature = "{CALL: " + (referenceNameElement != null ? referenceNameElement.getText() : null) + "}"; + } + else { + tokenFeature = viaResolve(expression); + } + if (tokenFeature != null) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(expression, tokenFeature); + } + super.visitMethodReferenceExpression(expression); + } + + @Override + public void visitArrayAccessExpression(PsiArrayAccessExpression expression) { + PsiExpression arrayIndex = expression.getIndexExpression(); + String tokenFeature = "{ARRAY: data: " + expression.getType() + " index:" + getTypeRepresentation(arrayIndex) + "}"; + myUsageSimilarityFeaturesRecorder.addAllFeatures(expression, tokenFeature); + super.visitArrayAccessExpression(expression); + } + + @Override + public void visitMethodCallExpression(PsiMethodCallExpression expression) { + String tokenFeature; + if (Registry.is("similarity.find.usages.fast.clustering")) { + tokenFeature = "{CALL: " + expression.getMethodExpression().getReferenceName() + "}"; + } + else { + tokenFeature = getTokenFeatureViaResolve(expression); + } + if (tokenFeature != null) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(expression, tokenFeature); + } + super.visitMethodCallExpression(expression); + } + + @Override + public void visitLiteralExpression(PsiLiteralExpression expression) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(expression, "literal:" + getTypeRepresentation(expression)); + super.visitLiteralExpression(expression); + } + + @Override + public void visitLambdaExpression(PsiLambdaExpression expression) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(expression, "lambda"); + super.visitLambdaExpression(expression); + } + + @Override + public void visitAnonymousClass(PsiAnonymousClass clazz) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(clazz, "anonymousClazz"); + super.visitAnonymousClass(clazz); + } + + private static @Nullable String viaResolve(@NotNull PsiMethodReferenceExpression expression) { + final PsiElement resolve = expression.resolve(); + final PsiMethod method = ObjectUtils.tryCast(resolve, PsiMethod.class); + if (method != null) { + final PsiClass containingClass = method.getContainingClass(); + if (containingClass != null) { + return "{CALL: " + + containingClass.getQualifiedName() + + "." + + method.getName() + "()" + + " ret:" + + getTypeRepresentation(expression) + "}"; + } + } + return null; + } + + public static @NotNull String getTypeRepresentation(@Nullable PsiExpression expression) { + if (expression == null) return "null"; + return getTypeRepresentation(expression.getType()); + } + + private static @NotNull String getTypeRepresentation(@Nullable PsiType type) { + if (type != null) { + return type.getCanonicalText(); + } + else { + return "null"; + } + } + + private static @Nullable String getTokenFeatureViaResolve(@NotNull PsiMethodCallExpression expression) { + String tokenFeature = null; + final PsiReferenceExpression referenceExpression = expression.getMethodExpression(); + final PsiElement resolve = referenceExpression.resolve(); + final PsiMethod method = ObjectUtils.tryCast(resolve, PsiMethod.class); + if (method != null) { + final PsiClass containingClass = method.getContainingClass(); + if (containingClass != null) { + tokenFeature = "{CALL: " + + containingClass.getQualifiedName() + + "." + + method.getName() + "()" + + " ret:" + + getTypeRepresentation(referenceExpression) + " " + + getArgumentData(expression) + "}"; + } + } + return tokenFeature; + } + + public static @NotNull String getArgumentData(@NotNull PsiMethodCallExpression methodCallExpression) { + PsiExpression[] expressions = methodCallExpression.getArgumentList().getExpressions(); + StringBuilder arguments = new StringBuilder(); + for (int i = 0; i < expressions.length; i++) { + arguments.append("arg").append(i).append(" type: ").append(getTypeRepresentation(expressions[i])); + } + return arguments.toString(); + } +} diff --git a/java/java-impl/src/com/intellij/find/findUsages/similarity/JavaUsageSimilarityFeaturesProvider.java b/java/java-impl/src/com/intellij/find/findUsages/similarity/JavaUsageSimilarityFeaturesProvider.java new file mode 100644 index 000000000000..42663d1cc3c3 --- /dev/null +++ b/java/java-impl/src/com/intellij/find/findUsages/similarity/JavaUsageSimilarityFeaturesProvider.java @@ -0,0 +1,74 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.find.findUsages.similarity; + +import com.intellij.openapi.util.registry.Registry; +import com.intellij.psi.*; +import com.intellij.psi.impl.source.PsiMethodImpl; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.usages.PsiElementUsageTarget; +import com.intellij.usages.similarity.bag.Bag; +import com.intellij.usages.similarity.features.UsageSimilarityFeaturesProvider; +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread; +import com.intellij.util.concurrency.annotations.RequiresReadLock; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class JavaUsageSimilarityFeaturesProvider implements UsageSimilarityFeaturesProvider { + + @Override + @RequiresReadLock + @RequiresBackgroundThread + public @NotNull Bag getFeatures(@NotNull PsiElement usage) { + PsiElement statement = getContainingStatement(usage); + if (statement != null) { + final Bag usageProperties = new JavaSimilarityPropertiesExtractor(statement).getFeatures(); + if (Registry.is("similarity.find.usages.parent.statement.condition.feature")) { + usageProperties.addAll(getParentStatementFeatures(statement)); + } + return usageProperties; + } + return Bag.EMPTY_BAG; + } + + @Override + @RequiresReadLock + public boolean isAvailable(@NotNull PsiElementUsageTarget usageTarget) { + return usageTarget.getElement() instanceof PsiMethodImpl; + } + + public @Nullable PsiElement getContainingStatement(@NotNull PsiElement element) { + return PsiTreeUtil.getParentOfType(element, PsiDeclarationStatement.class, + PsiExpressionStatement.class, + PsiIfStatement.class, + PsiWhileStatement.class, + PsiTryStatement.class, + PsiThrowStatement.class, + PsiSwitchStatement.class, + PsiReturnStatement.class, + PsiLoopStatement.class, + PsiImportStatement.class, + PsiForStatement.class, + PsiForeachStatement.class, + PsiConditionalLoopStatement.class, + PsiBlockStatement.class); + } + + private static @NotNull Bag getParentStatementFeatures(@NotNull PsiElement context) { + final PsiElement parentControlStatement = + PsiTreeUtil.findFirstParent(context, true, + element -> element instanceof PsiConditionalLoopStatement || element instanceof PsiIfStatement); + if (parentControlStatement instanceof PsiConditionalLoopStatement) { + final PsiExpression conditionExpression = ((PsiConditionalLoopStatement)parentControlStatement).getCondition(); + if (conditionExpression != null) { + return new JavaSimilarityPropertiesExtractor(conditionExpression).getFeatures(); + } + } + if (parentControlStatement instanceof PsiIfStatement) { + final PsiExpression conditionExpression = ((PsiIfStatement)parentControlStatement).getCondition(); + if (conditionExpression != null) { + return new JavaSimilarityPropertiesExtractor(conditionExpression).getFeatures(); + } + } + return Bag.EMPTY_BAG; + } +} diff --git a/java/java-tests/testData/findSimilar/Anonymous.java b/java/java-tests/testData/findSimilar/Anonymous.java new file mode 100644 index 000000000000..e6a25e55d210 --- /dev/null +++ b/java/java-tests/testData/findSimilar/Anonymous.java @@ -0,0 +1,12 @@ +class A { + public int foo(A a) { + return 0; + } + + public static void main(String[] args) { + A a1 = new A(); + int a = a1.foo(new A() { + public int foo(A a) { return 1; } + }); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/findSimilar/FeaturesProvider.java b/java/java-tests/testData/findSimilar/FeaturesProvider.java new file mode 100644 index 000000000000..1c9e1d8e0af1 --- /dev/null +++ b/java/java-tests/testData/findSimilar/FeaturesProvider.java @@ -0,0 +1,8 @@ +class A { + public int foo(A a){ + return 0; + } + public static void main(String[] args) { + A a1 = new A(); int a = a1.foo(new A()); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/findSimilar/ForFeatures.java b/java/java-tests/testData/findSimilar/ForFeatures.java new file mode 100644 index 000000000000..da17d8e4914b --- /dev/null +++ b/java/java-tests/testData/findSimilar/ForFeatures.java @@ -0,0 +1,10 @@ +class A { + public int foo(A a){ + return 0; + } + public static void main(String[] args) { + for (int a=0; ioo(this); i++) { + + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/findSimilar/Increment.java b/java/java-tests/testData/findSimilar/Increment.java new file mode 100644 index 000000000000..5527e1ab48f7 --- /dev/null +++ b/java/java-tests/testData/findSimilar/Increment.java @@ -0,0 +1,9 @@ + + +class A { + public int af() { return 1; } + public static void main(String[] args) { + int a = 0; + a += af(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/findSimilar/Lambda.java b/java/java-tests/testData/findSimilar/Lambda.java new file mode 100644 index 000000000000..0848342a0130 --- /dev/null +++ b/java/java-tests/testData/findSimilar/Lambda.java @@ -0,0 +1,12 @@ +package aPackage; + +class A { + public int foo(Runnable run) { + return 0; + } + + public static void main(String[] args) { + A a1 = new A(); + int a = a1.foo(()->{}); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/findSimilar/MethodReference.java b/java/java-tests/testData/findSimilar/MethodReference.java new file mode 100644 index 000000000000..8c4d8cd10566 --- /dev/null +++ b/java/java-tests/testData/findSimilar/MethodReference.java @@ -0,0 +1,14 @@ +package aPackage; + +class A { + public int foo(Runnable run) { + return 0; + } + + public static void main(String[] args) { + A a1 = new A(); + int a = a1.foo(A::run); + } + + private static void run() { } +} \ No newline at end of file diff --git a/java/java-tests/testData/findSimilar/usageGroups/arrayAccess/Market.java b/java/java-tests/testData/findSimilar/usageGroups/arrayAccess/Market.java new file mode 100644 index 000000000000..ed899f6dde07 --- /dev/null +++ b/java/java-tests/testData/findSimilar/usageGroups/arrayAccess/Market.java @@ -0,0 +1,17 @@ +import org.jetbrains.annotations.NotNull; + +public class Market { + + public void a() { + System.out.println(getGoods()[0]); + System.out.println(getGoods()[1]); + System.out.println(getGoods()[1].charAt(0)); + System.out.println(getGoods()[0].charAt(0)); + System.out.println(getGoods()[0]+ getGoods()[1]); + } + + @NotNull + private static String[] getGoods() { + return new String[]{"orange", "apple", "cucumber", "tomato"}; + } +} diff --git a/java/java-tests/testData/findSimilar/usageGroups/arrayAccess/results.txt b/java/java-tests/testData/findSimilar/usageGroups/arrayAccess/results.txt new file mode 100644 index 000000000000..7339b099bd7b --- /dev/null +++ b/java/java-tests/testData/findSimilar/usageGroups/arrayAccess/results.txt @@ -0,0 +1,10 @@ +[{id=0 +, myUsages=6|System.out.println(|getGoods|()[|0|]);, +7|System.out.println(|getGoods|()[|1|]);} +, {id=1 +, myUsages=8|System.out.println(|getGoods|()[|1|].charAt(|0|));, +9|System.out.println(|getGoods|()[|0|].charAt(|0|));} +, {id=2 +, myUsages=10|System.out.println(|getGoods|()[|0|]+ getGoods()[|1|]);, +10|System.out.println(getGoods()[|0|]+ |getGoods|()[|1|]);} +] diff --git a/java/java-tests/testData/findSimilar/usageGroups/binaryExpressions/UsageGroups.java b/java/java-tests/testData/findSimilar/usageGroups/binaryExpressions/UsageGroups.java new file mode 100644 index 000000000000..f65f84464830 --- /dev/null +++ b/java/java-tests/testData/findSimilar/usageGroups/binaryExpressions/UsageGroups.java @@ -0,0 +1,14 @@ +class Foo { + public int f() { + return 1; + } +} +class D { + public void run() { + Foo foo = new Foo() + foo.f() + foo.f(); + foo.f()+foo.f(); + int ff1 = foo.f() +1; + int ff2 = foo.f() +2; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/findSimilar/usageGroups/binaryExpressions/results.txt b/java/java-tests/testData/findSimilar/usageGroups/binaryExpressions/results.txt new file mode 100644 index 000000000000..1ccee9c89691 --- /dev/null +++ b/java/java-tests/testData/findSimilar/usageGroups/binaryExpressions/results.txt @@ -0,0 +1,9 @@ +[{id=0 +, myUsages=9|foo.|f|() + foo.f();, +9|foo.f() + foo.|f|();, +10|foo.|f|()+foo.f();, +10|foo.f()+foo.|f|();} +, {id=1 +, myUsages=11|int| ff1 = foo.|f|() +|1|;, +12|int| ff2 = foo.|f|() +|2|;} +] diff --git a/java/java-tests/testData/findSimilar/usageGroups/mapUsages/MapUsages.java b/java/java-tests/testData/findSimilar/usageGroups/mapUsages/MapUsages.java new file mode 100644 index 000000000000..6529424bbaf6 --- /dev/null +++ b/java/java-tests/testData/findSimilar/usageGroups/mapUsages/MapUsages.java @@ -0,0 +1,22 @@ +import java.util.HashMap; +import java.util.Map; + +public class MapUsages { + + public void a() { + Map map =new HashMap<>(); + //init map + map.put("1", "first"); + map.put("2", "second"); + //verify map + if (!map.containsKey("1")) { + map.put("1", "first"); + } + if (!map.containsKey("2")) { + map.put("2", "second"); + } + //clear data + map.put("1", null); + map.put("2", null); + } +} diff --git a/java/java-tests/testData/findSimilar/usageGroups/mapUsages/results.txt b/java/java-tests/testData/findSimilar/usageGroups/mapUsages/results.txt new file mode 100644 index 000000000000..1606e596143c --- /dev/null +++ b/java/java-tests/testData/findSimilar/usageGroups/mapUsages/results.txt @@ -0,0 +1,10 @@ +[{id=0 +, myUsages=9|map.|put|(|"1"|, |"first"|);, +10|map.|put|(|"2"|, |"second"|);} +, {id=1 +, myUsages=13|map.|put|(|"1"|, |"first"|);, +16|map.|put|(|"2"|, |"second"|);} +, {id=2 +, myUsages=19|map.|put|(|"1"|, |null|);, +20|map.|put|(|"2"|, |null|);} +] diff --git a/java/java-tests/testData/findSimilar/usageGroups/streamUsages/StreamUsages.java b/java/java-tests/testData/findSimilar/usageGroups/streamUsages/StreamUsages.java new file mode 100644 index 000000000000..1468cd49aea9 --- /dev/null +++ b/java/java-tests/testData/findSimilar/usageGroups/streamUsages/StreamUsages.java @@ -0,0 +1,23 @@ +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class CollectionsPlayGround { + + public static void main(String[] args) { + List list = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + list.add("entry:" + i); + } + + final List unmodifiableHashcodes = list.stream().map(String::hashCode).collect(Collectors.toUnmodifiableList()); + final List modifiableHashcodes = list.stream().map(String::hashCode).collect(Collectors.toList()); + + List nonEmptyEntries = list.stream().filter(e -> e.length() > 0).collect(Collectors.toList()); + List moreThanOneCharEntry = list.stream().filter(e -> e.length() > 1).collect(Collectors.toList()); + List nonEmptyEntries2 = list.stream().filter(e -> e.isEmpty() ).collect(Collectors.toList()); + + Set nonEmptySet = list.stream().filter(e -> e.length() > 0).collect(Collectors.toSet()); + } +} diff --git a/java/java-tests/testData/findSimilar/usageGroups/streamUsages/results.txt b/java/java-tests/testData/findSimilar/usageGroups/streamUsages/results.txt new file mode 100644 index 000000000000..702ead30b938 --- /dev/null +++ b/java/java-tests/testData/findSimilar/usageGroups/streamUsages/results.txt @@ -0,0 +1,12 @@ +[{id=0 +, myUsages=14|final| List unmodifiableHashcodes = list.|stream|().map(String::hashCode).collect(Collectors.toUnmodifiableList());} +, {id=1 +, myUsages=15|final| List modifiableHashcodes = list.|stream|().map(String::hashCode).collect(Collectors.toList());} +, {id=2 +, myUsages=17|List nonEmptyEntries = list.|stream|().filter(e -> e.length() > |0|).collect(Collectors.toList());, +18|List moreThanOneCharEntry = list.|stream|().filter(e -> e.length() > |1|).collect(Collectors.toList());} +, {id=3 +, myUsages=19|List nonEmptyEntries2 = list.|stream|().filter(e -> e.isEmpty() ).collect(Collectors.toList());} +, {id=4 +, myUsages=21|Set nonEmptySet = list.|stream|().filter(e -> e.length() > |0|).collect(Collectors.toSet());} +] diff --git a/java/java-tests/testSrc/com/intellij/find/groupSimilar/JavaFindUsagesGroupsTest.java b/java/java-tests/testSrc/com/intellij/find/groupSimilar/JavaFindUsagesGroupsTest.java new file mode 100644 index 000000000000..ab692c4cbe42 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/find/groupSimilar/JavaFindUsagesGroupsTest.java @@ -0,0 +1,69 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.find.groupSimilar; + +import com.intellij.JavaTestUtil; +import com.intellij.find.findUsages.JavaFindUsagesHandler; +import com.intellij.find.findUsages.JavaFindUsagesHandlerFactory; +import com.intellij.openapi.util.registry.Registry; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.testFramework.IdeaTestUtil; +import com.intellij.testFramework.JavaPsiTestCase; +import com.intellij.testFramework.PsiTestUtil; +import com.intellij.usages.UsageInfoToUsageConverter; +import com.intellij.usages.similarity.clustering.ClusteringSearchSession; +import org.jetbrains.annotations.NotNull; + +import java.io.File; + +import static com.intellij.testFramework.EqualsToFile.assertEqualsToFile; + +public class JavaFindUsagesGroupsTest extends JavaPsiTestCase { + @Override + protected void setUp() throws Exception { + super.setUp(); + PsiTestUtil.removeAllRoots(myModule, IdeaTestUtil.getMockJdk18()); + createTestProjectStructure(getTestRoot()); + } + + private @NotNull String getTestRoot() { + return JavaTestUtil.getJavaTestDataPath() + "/findSimilar/usageGroups/" + getTestName(true); + } + + private void doTest(@NotNull PsiMethod streamMethod) { + final JavaFindUsagesHandlerFactory factory = JavaFindUsagesHandlerFactory.getInstance(getProject()); + JavaFindUsagesHandler handler = new JavaFindUsagesHandler(streamMethod, factory); + ClusteringSearchSession session = new ClusteringSearchSession(); + handler.processElementUsages(streamMethod, info -> { + UsageInfoToUsageConverter.convertToSimilarUsage(new PsiMethod[]{streamMethod}, info, session); + return true; + }, factory.getFindMethodOptions()); + File file = new File(getTestRoot() + "/results.txt"); + assertEqualsToFile("", file, session.getClusters().toString()); + } + + private @NotNull PsiMethod getMethod(String className, String methodName) { + PsiClass anInterface = myJavaFacade.findClass(className, GlobalSearchScope.allScope(myProject)); + return anInterface.findMethodsByName(methodName, false)[0]; + } + + public void testStreamUsages() { + doTest(getMethod("java.util.Collection", "stream")); + } + + public void testBinaryExpressions() { + doTest(getMethod("Foo", "f")); + } + + public void testMapUsages() { + try { + Registry.get("similarity.find.usages.parent.statement.condition.feature").setValue(true); + doTest(getMethod("java.util.Map", "put")); + } finally { + Registry.get("similarity.find.usages.fast.clustering").resetToDefault(); + } + } + + public void testArrayAccess() { doTest(getMethod("Market", "getGoods")); } +} diff --git a/java/java-tests/testSrc/com/intellij/find/groupSimilar/JavaUsagesBySimilarityTest.java b/java/java-tests/testSrc/com/intellij/find/groupSimilar/JavaUsagesBySimilarityTest.java new file mode 100644 index 000000000000..37528accca7f --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/find/groupSimilar/JavaUsagesBySimilarityTest.java @@ -0,0 +1,109 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.find.groupSimilar; + +import com.intellij.JavaTestUtil; +import com.intellij.find.findUsages.similarity.JavaUsageSimilarityFeaturesProvider; +import com.intellij.openapi.util.registry.Registry; +import com.intellij.psi.PsiElement; +import com.intellij.testFramework.fixtures.JavaCodeInsightFixtureTestCase; +import com.intellij.usages.similarity.bag.Bag; +import com.intellij.usages.similarity.bag.BagsDistanceCalculator; + +public class JavaUsagesBySimilarityTest extends JavaCodeInsightFixtureTestCase { + @Override + protected String getTestDataPath() { + return JavaTestUtil.getJavaTestDataPath() + "/findSimilar/"; + } + + public void testFeaturesProvider() { + myFixture.configureByFile("FeaturesProvider.java"); + PsiElement elementAtCaret = myFixture.getReferenceAtCaretPosition().getElement(); + final Bag features = new JavaUsageSimilarityFeaturesProvider().getFeatures(elementAtCaret); + assertEquals(1, features.get("{CALL: foo}")); + assertEquals(1, features.get("NEW: A")); + assertEquals(1, features.get("NEW_KEYWORD")); + } + + public void testForFeatures() { + myFixture.configureByFile("ForFeatures.java"); + PsiElement elementAtCaret = myFixture.getReferenceAtCaretPosition().getElement(); + final Bag features = new JavaUsageSimilarityFeaturesProvider().getFeatures(elementAtCaret); + assertEquals(1, features.get("FOR")); + assertEquals(1, features.get("FOR_KEYWORD")); + assertEquals(1, features.get("GP:VAR: FOR_STATEMENT -1")); + } + + public void testAnonymous() { + try { + Registry.get("similarity.find.usages.fast.clustering").setValue(false); + myFixture.configureByFile("Anonymous.java"); + PsiElement elementAtCaret = myFixture.getReferenceAtCaretPosition().getElement(); + final Bag features = new JavaUsageSimilarityFeaturesProvider().getFeatures(elementAtCaret); + assertEquals(1, features.get("P:anonymousClazz NEW_EXPRESSION -1")); + assertEquals(1, features.get("NEXT: {CALL: A.foo() ret:int arg0 type: A} PsiJavaToken:SEMICOLON")); + } + finally { + Registry.get("similarity.find.usages.fast.clustering").resetToDefault(); + } + } + + public void testIncrement() { + try { + Registry.get("similarity.find.usages.fast.clustering").setValue(false); + myFixture.configureByFile("Increment.java"); + PsiElement elementAtCaret = myFixture.getReferenceAtCaretPosition().getElement(); + final Bag features = new JavaUsageSimilarityFeaturesProvider().getFeatures(elementAtCaret); + assertEquals(1, features.get("+=")); + assertEquals(1, features.get("{CALL: A.af() ret:int }")); + } + finally { + Registry.get("similarity.find.usages.fast.clustering").resetToDefault(); + } + } + + public void testLambda() { + try { + Registry.get("similarity.find.usages.fast.clustering").setValue(false); + myFixture.configureByFile("Lambda.java"); + PsiElement elementAtCaret = myFixture.getReferenceAtCaretPosition().getElement(); + final Bag features = new JavaUsageSimilarityFeaturesProvider().getFeatures(elementAtCaret); + assertEquals(1, features.get("{CALL: aPackage.A.foo() ret:int arg0 type: }")); + assertEquals(1, features.get("GP:lambda METHOD_CALL_EXPRESSION -1")); + } + finally { + Registry.get("similarity.find.usages.fast.clustering").resetToDefault(); + } + } + + public void testMethodReference() { + try { + Registry.get("similarity.find.usages.fast.clustering").setValue(false); + myFixture.configureByFile("MethodReference.java"); + + PsiElement elementAtCaret = myFixture.getReferenceAtCaretPosition().getElement(); + final Bag features = new JavaUsageSimilarityFeaturesProvider().getFeatures(elementAtCaret); + assertEquals(1, features.get("{CALL: aPackage.A.foo() ret:int arg0 type: }")); + } + finally { + Registry.get("similarity.find.usages.fast.clustering").resetToDefault(); + } + } + + public void testBag() { + final Bag bag = new Bag("a", "b"); + assertEquals("a : 1\n" + + "b : 1\n", bag.toString()); + bag.add("a"); + Bag toAdd = new Bag("a", "c"); + bag.addAll(toAdd); + assertEquals("c : 1\n" + + "a : 3\n" + + "b : 1\n", bag.toString()); + } + + public void testDistance() { + assertEquals(0.6, + BagsDistanceCalculator.jaccardSimilarity(new Bag("a", "b", "c", "d", "e", "f", "g", "h"), + new Bag("a", "z", "c", "y", "e", "f", "g", "h"))); + } +} diff --git a/platform/ide-core/src/com/intellij/openapi/actionSystem/ActionPlaces.java b/platform/ide-core/src/com/intellij/openapi/actionSystem/ActionPlaces.java index acc798021d13..cf8f29a9cfd1 100644 --- a/platform/ide-core/src/com/intellij/openapi/actionSystem/ActionPlaces.java +++ b/platform/ide-core/src/com/intellij/openapi/actionSystem/ActionPlaces.java @@ -67,6 +67,7 @@ public abstract class ActionPlaces { public static final String METHOD_HIERARCHY_VIEW_TOOLBAR = "MethodHierarchyViewToolbar"; public static final String CALL_HIERARCHY_VIEW_POPUP = "CallHierarchyViewPopup"; public static final String CALL_HIERARCHY_VIEW_TOOLBAR = "CallHierarchyViewToolbar"; + public static final String SIMILAR_USAGES_PREVIEW_TOOLBAR = "SimilarUsagesPreviewToolbar"; public static final String J2EE_ATTRIBUTES_VIEW_POPUP = "J2EEAttributesViewPopup"; public static final String J2EE_VIEW_POPUP = "J2EEViewPopup"; public static final String RUNNER_TOOLBAR = "RunnerToolbar"; @@ -200,7 +201,7 @@ public abstract class ActionPlaces { UNKNOWN, KEYBOARD_SHORTCUT, MOUSE_SHORTCUT, FORCE_TOUCH, TOOLBAR, MAIN_MENU, MAIN_TOOLBAR, EDITOR_TOOLBAR, TABS_MORE_TOOLBAR, EDITOR_TAB, COMMANDER_TOOLBAR, CONTEXT_TOOLBAR, TOOLWINDOW_TITLE, LEARN_TOOLWINDOW, PROJECT_VIEW_TOOLBAR, STATUS_BAR_PLACE, ACTION_SEARCH, TESTTREE_VIEW_TOOLBAR, TYPE_HIERARCHY_VIEW_TOOLBAR, - METHOD_HIERARCHY_VIEW_TOOLBAR, CALL_HIERARCHY_VIEW_TOOLBAR, RUNNER_TOOLBAR, DEBUGGER_TOOLBAR, USAGE_VIEW_TOOLBAR, + METHOD_HIERARCHY_VIEW_TOOLBAR, CALL_HIERARCHY_VIEW_TOOLBAR, SIMILAR_USAGES_PREVIEW_TOOLBAR, RUNNER_TOOLBAR, DEBUGGER_TOOLBAR, USAGE_VIEW_TOOLBAR, SHOW_USAGES_POPUP_TOOLBAR, STRUCTURE_VIEW_TOOLBAR, NAVIGATION_BAR_TOOLBAR, TODO_VIEW_TOOLBAR, COMPILER_MESSAGES_TOOLBAR, ANT_MESSAGES_TOOLBAR, ANT_EXPLORER_TOOLBAR, CODE_INSPECTION, JAVADOC_TOOLBAR, JAVADOC_INPLACE_SETTINGS, diff --git a/platform/lang-api/resources/messages/CodeInsightBundle.properties b/platform/lang-api/resources/messages/CodeInsightBundle.properties index 2b069bb4c52c..31849d09df81 100644 --- a/platform/lang-api/resources/messages/CodeInsightBundle.properties +++ b/platform/lang-api/resources/messages/CodeInsightBundle.properties @@ -587,4 +587,4 @@ documentation.file.modification.datetime.label=Modified: documentation.file.scope.label=Scope: documentation.file.vcs.status.label=VCS Status: notification.group.dependencies=Module dependencies analysis completed -notification.group.parameter.name.hints=Parameter name hints settings changed +notification.group.parameter.name.hints=Parameter name hints settings changed \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/find/findUsages/FindUsagesManager.java b/platform/lang-impl/src/com/intellij/find/findUsages/FindUsagesManager.java index 25f5aa1a1487..0b092b60f7b0 100644 --- a/platform/lang-impl/src/com/intellij/find/findUsages/FindUsagesManager.java +++ b/platform/lang-impl/src/com/intellij/find/findUsages/FindUsagesManager.java @@ -47,6 +47,7 @@ import com.intellij.usageView.UsageViewContentManager; import com.intellij.usageView.UsageViewUtil; import com.intellij.usages.*; import com.intellij.usages.impl.UsageViewStatisticsCollector; +import com.intellij.usages.similarity.clustering.ClusteringSearchSession; import com.intellij.util.ArrayUtil; import com.intellij.util.CommonProcessors; import com.intellij.util.Processor; @@ -357,6 +358,7 @@ public final class FindUsagesManager { }); FindUsagesOptions optionsClone = options.clone(); + ClusteringSearchSession clusteringSearchSession = ClusteringSearchSession.createClusteringSessionIfEnabled(); return processor -> { Project project = ReadAction.compute(() -> scopeFile != null ? scopeFile.getProject() : primaryTargets[0].getProject()); ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); @@ -375,7 +377,11 @@ public final class FindUsagesManager { optionsClone.searchScope = new LocalSearchScope(scopeFile); } Processor usageInfoProcessor = new CommonProcessors.UniqueProcessor<>(usageInfo -> { - Usage usage = ReadAction.compute(() -> UsageInfoToUsageConverter.convert(primaryElements, usageInfo)); + Usage usage = ReadAction.compute( + () -> clusteringSearchSession != null + ? UsageInfoToUsageConverter.convertToSimilarUsage(primaryElements, usageInfo, clusteringSearchSession) + : UsageInfoToUsageConverter.convert(primaryElements, usageInfo) + ); return processor.process(usage); }); PsiElement[] elements = ArrayUtil.mergeArrays(primaryElements, secondaryElements, PsiElement.ARRAY_FACTORY); diff --git a/platform/platform-resources-en/src/messages/ActionsBundle.properties b/platform/platform-resources-en/src/messages/ActionsBundle.properties index 5b0c28eba5d2..d4d1b1b4c8c0 100644 --- a/platform/platform-resources-en/src/messages/ActionsBundle.properties +++ b/platform/platform-resources-en/src/messages/ActionsBundle.properties @@ -2465,6 +2465,7 @@ group.Internal.Java.text=Java action.CollectFUStatisticsAction.text=Collect Statistics from Collector... action.TestParseEventsSchemeAction.text=Test Parsing Events Scheme... action.ValidateLocalHistory.text=Validate Local History Storage +action.ShowUsageFeatures.text=Show Usage Features action.FixLineSeparators.text=Fix Line Separators action.PruneEmptyDirectories.text=Prune Empty Directories action.LoadAllVFSContent.text=Load All VFS Stored Files Content diff --git a/platform/platform-resources/src/META-INF/LangExtensionPoints.xml b/platform/platform-resources/src/META-INF/LangExtensionPoints.xml index 1839fd3697a8..09fb6ef15417 100644 --- a/platform/platform-resources/src/META-INF/LangExtensionPoints.xml +++ b/platform/platform-resources/src/META-INF/LangExtensionPoints.xml @@ -399,6 +399,7 @@ + diff --git a/platform/platform-resources/src/META-INF/LangExtensions.xml b/platform/platform-resources/src/META-INF/LangExtensions.xml index 0e47048faeae..a303903a3130 100644 --- a/platform/platform-resources/src/META-INF/LangExtensions.xml +++ b/platform/platform-resources/src/META-INF/LangExtensions.xml @@ -1165,6 +1165,7 @@ + diff --git a/platform/platform-resources/src/idea/LangActions.xml b/platform/platform-resources/src/idea/LangActions.xml index d40d77ebdc71..5c4a83d23f30 100644 --- a/platform/platform-resources/src/idea/LangActions.xml +++ b/platform/platform-resources/src/idea/LangActions.xml @@ -1707,5 +1707,6 @@ + diff --git a/platform/usageView-impl/src/com/intellij/find/findUsages/similarity/LocationLinkComponent.java b/platform/usageView-impl/src/com/intellij/find/findUsages/similarity/LocationLinkComponent.java new file mode 100644 index 000000000000..7ee28b9a5eda --- /dev/null +++ b/platform/usageView-impl/src/com/intellij/find/findUsages/similarity/LocationLinkComponent.java @@ -0,0 +1,39 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.find.findUsages.similarity; + +import com.intellij.openapi.util.Iconable; +import com.intellij.psi.PsiFile; +import com.intellij.ui.components.ActionLink; +import com.intellij.usageView.UsageInfo; +import com.intellij.util.IconUtil; +import com.intellij.util.PsiNavigateUtil; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.util.Objects; + +public class LocationLinkComponent { + + @NotNull + private final ActionLink myLabel; + + public LocationLinkComponent(@NotNull UsageInfo item) { + PsiFile file = item.getFile(); + myLabel = new ActionLink(Objects.requireNonNull(file).getName(), new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + PsiNavigateUtil.navigate(item.getElement()); + } + }); + + myLabel.setBackground(UIUtil.TRANSPARENT_COLOR); + myLabel.setOpaque(false); + myLabel.setIcon(IconUtil.getIcon(item.getFile().getVirtualFile(), Iconable.ICON_FLAG_READ_STATUS, item.getProject())); + } + + public @NotNull JComponent getComponent() { + return myLabel; + } +} diff --git a/platform/usageView-impl/src/com/intellij/find/findUsages/similarity/MostCommonUsagePatternsComponent.java b/platform/usageView-impl/src/com/intellij/find/findUsages/similarity/MostCommonUsagePatternsComponent.java new file mode 100644 index 000000000000..f411638cbebe --- /dev/null +++ b/platform/usageView-impl/src/com/intellij/find/findUsages/similarity/MostCommonUsagePatternsComponent.java @@ -0,0 +1,250 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.find.findUsages.similarity; + +import com.intellij.icons.AllIcons; +import com.intellij.ide.ActivityTracker; +import com.intellij.ide.IdeBundle; +import com.intellij.ide.actions.RefreshAction; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.SimpleToolWindowPanel; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.Ref; +import com.intellij.ui.Gray; +import com.intellij.ui.JBColor; +import com.intellij.ui.ScrollPaneFactory; +import com.intellij.ui.components.ActionLink; +import com.intellij.ui.components.JBPanelWithEmptyText; +import com.intellij.ui.components.panels.VerticalLayout; +import com.intellij.usageView.UsageInfo; +import com.intellij.usageView.UsageViewBundle; +import com.intellij.usages.Usage; +import com.intellij.usages.UsageGroup; +import com.intellij.usages.UsageInfo2UsageAdapter; +import com.intellij.usages.UsageView; +import com.intellij.usages.impl.UsageViewImpl; +import com.intellij.usages.similarity.clustering.ClusteringSearchSession; +import com.intellij.usages.similarity.clustering.UsageCluster; +import com.intellij.usages.similarity.usageAdapter.SimilarityUsage; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.ui.JBUI; +import com.intellij.util.ui.scroll.BoundedRangeModelThresholdListener; +import kotlin.Unit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.util.List; +import java.util.*; + +import static com.intellij.openapi.actionSystem.ActionPlaces.SIMILAR_USAGES_PREVIEW_TOOLBAR; + +public class MostCommonUsagePatternsComponent extends SimpleToolWindowPanel implements Disposable { + private static final int CLUSTER_LIMIT = 20; + private final @NotNull Project myProject; + private final @NotNull Collection> myGroups; + private final @NotNull UsageViewImpl myUsageView; + private final @NotNull JBPanelWithEmptyText myMainPanel; + private final @NotNull JScrollPane myScrollPane; + private @Nullable ClusteringSearchSession mySession; + private boolean myIsShowingSimilarUsages; + + private boolean isDisposed = false; + private @Nullable BackgroundableProcessIndicator myProcessIndicator; + private int myAlreadyRenderedSnippets; + + public MostCommonUsagePatternsComponent(@NotNull UsageViewImpl usageView, + @NotNull Collection<@NotNull Collection> groups) { + super(false); + myUsageView = usageView; + myProject = usageView.getProject(); + myGroups = groups; + setToolbar((JComponent)createToolbar(SIMILAR_USAGES_PREVIEW_TOOLBAR)); + myMainPanel = new JBPanelWithEmptyText(); + myMainPanel.setLayout(new VerticalLayout(0)); + addMostCommonUsagesForSelectedGroups(myGroups); + myScrollPane = ScrollPaneFactory.createScrollPane(myMainPanel); + myScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + revalidate(); + setContent(myScrollPane); + } + + @Nullable + public ClusteringSearchSession getSession() { + if (mySession == null) { + mySession = findClusteringSessionInUsageView(myUsageView); + } + return mySession; + } + + public ActionToolbar createToolbar(@NotNull String place) { + DefaultActionGroup actionGroup = new DefaultActionGroup(); + actionGroup.add( + new RefreshAction(IdeBundle.messagePointer("action.refresh"), IdeBundle.messagePointer("action.refresh"), AllIcons.Actions.Refresh) { + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + myMainPanel.removeAll(); + myMainPanel.revalidate(); + myIsShowingSimilarUsages = false; + addMostCommonUsagesForSelectedGroups(myGroups); + } + + @Override + public void update(@NotNull AnActionEvent event) { + Presentation presentation = event.getPresentation(); + presentation.setEnabled(true); + } + }); + actionGroup.add( + new AnAction(IdeBundle.messagePointer("action.Anonymous.text.back"), IdeBundle.messagePointer("action.Anonymous.text.back"), + AllIcons.Actions.Back) { + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + if (myIsShowingSimilarUsages) { + myIsShowingSimilarUsages = false; + ActivityTracker.getInstance().inc(); + setContent(myScrollPane); + revalidate(); + } + } + + @Override + public void update(@NotNull AnActionEvent event) { + Presentation presentation = event.getPresentation(); + presentation.setEnabled(myIsShowingSimilarUsages); + } + }); + ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar(place, actionGroup, false); + actionToolbar.setTargetComponent(myMainPanel); + return actionToolbar; + } + + private @NotNull ActionLink createOpenSimilarUsagesActionLink(UsageInfo info, @NotNull Set usagesToRender) { + final ActionLink actionLink = new ActionLink(UsageViewBundle.message("show.0.similar.usages", usagesToRender.size() - 1), e -> { + myIsShowingSimilarUsages = true; + ActivityTracker.getInstance().inc(); + final SimilarUsagesComponent mySimilarComponent = new SimilarUsagesComponent(info, this); + JScrollPane scroll = ScrollPaneFactory.createScrollPane(mySimilarComponent); + final JScrollBar scrollBar = scroll.getVerticalScrollBar(); + mySimilarComponent.renderOriginalUsage(); + mySimilarComponent.renderSimilarUsages(usagesToRender); + BoundedRangeModelThresholdListener.Companion.install(scrollBar, () -> { + mySimilarComponent.renderSimilarUsages(usagesToRender); + return Unit.INSTANCE; + }); + setContent(scroll); + revalidate(); + }); + actionLink.setLinkIcon(); + return actionLink; + } + + @Override + public void dispose() { + isDisposed = true; + if (myProcessIndicator != null && myProcessIndicator.isRunning()) { + myProcessIndicator.cancel(); + } + } + + private @NotNull JPanel createSummaryComponent(@Nullable ClusteringSearchSession session, + @NotNull List topClusters) { + JPanel summaryPanel = new JPanel(new VerticalLayout(0)); + if (session == null) return summaryPanel; + myAlreadyRenderedSnippets = 0; + topClusters.stream().skip(myAlreadyRenderedSnippets).limit(CLUSTER_LIMIT).forEach(cluster -> { + renderCluster(summaryPanel, cluster); + }); + final JScrollBar verticalScrollBar = myScrollPane.getVerticalScrollBar(); + BoundedRangeModelThresholdListener.Companion.install(verticalScrollBar, () -> { + topClusters.stream().skip(myAlreadyRenderedSnippets).limit(CLUSTER_LIMIT).forEach(cluster -> { + renderCluster(summaryPanel, cluster); + }); + return Unit.INSTANCE; + }); + return summaryPanel; + } + + private void renderCluster(@NotNull JPanel summaryPanel, + @NotNull UsageCluster cluster) { + final Set usageFilteredByGroup = new HashSet<>(); + ApplicationManager.getApplication().runReadAction(() -> { + usageFilteredByGroup.addAll(cluster.getUsageFilteredByGroup(myGroups)); + }); + SimilarityUsage usage = ContainerUtil.getFirstItem(usageFilteredByGroup); + if (usage instanceof UsageInfo2UsageAdapter) { + final UsageInfo2UsageAdapter usageInfoAdapter = (UsageInfo2UsageAdapter)usage; + UsageCodeSnippetComponent summaryRendererComponent = + UsageCodeSnippetComponent.createUsageCodeSnippet(usageInfoAdapter.getUsageInfo()); + if (!isDisposed) { + Disposer.register(this, summaryRendererComponent); + } + final JPanel headerPanel = + createHeaderPanel(usageInfoAdapter.getUsageInfo(), summaryRendererComponent.getEditor().getBackgroundColor()); + if (usageFilteredByGroup.size() > 1) { + headerPanel.add(createOpenSimilarUsagesActionLink(usageInfoAdapter.getUsageInfo(), usageFilteredByGroup)); + } + summaryPanel.add(headerPanel); + summaryPanel.add(summaryRendererComponent); + myAlreadyRenderedSnippets++; + } + } + + private void addMostCommonUsagesForSelectedGroups(@NotNull Collection> selectedGroups) { + Ref> sortedClusters = new Ref<>(); + Task.Backgroundable loadMostCommonUsagePatternsTask = + new Task.Backgroundable(myProject, UsageViewBundle.message("loading.most.common.usage.patterns")) { + @Override + public void onSuccess() { + if (!sortedClusters.isNull()) { + myMainPanel.add(createSummaryComponent(getSession(), sortedClusters.get())); + myMainPanel.revalidate(); + myIsShowingSimilarUsages = false; + } + } + + @Override + public void run(@NotNull ProgressIndicator indicator) { + ClusteringSearchSession session = getSession(); + if (session != null) { + final List groups = new ArrayList<>(); + ApplicationManager.getApplication().runReadAction(() -> { + groups.addAll(session.getClustersFilteredByGroups(indicator, selectedGroups)); + }); + sortedClusters.set(groups); + } + } + }; + myProcessIndicator = new BackgroundableProcessIndicator(loadMostCommonUsagePatternsTask); + ProgressManager.getInstance().runProcessWithProgressAsynchronously(loadMostCommonUsagePatternsTask, + myProcessIndicator); + } + + public static @Nullable ClusteringSearchSession findClusteringSessionInUsageView(@NotNull UsageView usageView) { + Optional usage = usageView.getUsages().stream().findFirst(); + if (usage.isPresent()) { + Usage firstUsage = usage.get(); + if (firstUsage instanceof SimilarityUsage) { + return ((SimilarityUsage)firstUsage).getClusteringSession(); + } + } + return null; + } + + public static @NotNull JPanel createHeaderPanel(@NotNull UsageInfo info, @NotNull Color backgroundColor) { + final LocationLinkComponent component = new LocationLinkComponent(info); + final JPanel header = new JPanel(); + header.setBackground(backgroundColor); + header.setLayout(new FlowLayout(FlowLayout.LEFT)); + header.add(component.getComponent()); + header.setBorder(JBUI.Borders.customLineTop(new JBColor(Gray.xCD, Gray.x51))); + return header; + } +} diff --git a/platform/usageView-impl/src/com/intellij/find/findUsages/similarity/SimilarUsagesComponent.java b/platform/usageView-impl/src/com/intellij/find/findUsages/similarity/SimilarUsagesComponent.java new file mode 100644 index 000000000000..604df1a823f4 --- /dev/null +++ b/platform/usageView-impl/src/com/intellij/find/findUsages/similarity/SimilarUsagesComponent.java @@ -0,0 +1,81 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.find.findUsages.similarity; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.util.Disposer; +import com.intellij.psi.PsiElement; +import com.intellij.ui.*; +import com.intellij.ui.components.panels.VerticalLayout; +import com.intellij.usageView.UsageInfo; +import com.intellij.usageView.UsageViewBundle; +import com.intellij.usages.UsageInfo2UsageAdapter; +import com.intellij.usages.similarity.usageAdapter.SimilarityUsage; +import com.intellij.util.ui.JBUI; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; +import java.util.Collection; + +public class SimilarUsagesComponent extends JPanel implements Disposable { + + public static final int SNIPPET_LIMIT = 10; + private int alreadyShown = 0; + private final @NotNull UsageInfo myOriginalUsage; + + public SimilarUsagesComponent(@NotNull UsageInfo originalUsage, @NotNull Disposable parent) { + myOriginalUsage = originalUsage; + setLayout(new VerticalLayout(0)); + TitlePanel titlePanel = new TitlePanel(); + titlePanel.getLabel().setHorizontalAlignment(SwingConstants.LEFT); + titlePanel.setText(UsageViewBundle.message("label.similar.usages")); + add(titlePanel); + Disposer.register(parent, this); + } + + public void renderSimilarUsages(@NotNull Collection similarUsagesGroupUsages) { + if (alreadyShown < similarUsagesGroupUsages.size() - 1) { + similarUsagesGroupUsages.stream().skip(alreadyShown).limit(SNIPPET_LIMIT).forEach(usage -> { + final UsageInfo info = ((UsageInfo2UsageAdapter)usage).getUsageInfo(); + if (myOriginalUsage != info) { + renderUsage(info); + alreadyShown++; + } + }); + } + } + + private void renderUsage(@NotNull UsageInfo info) { + PsiElement element = info.getElement(); + if (element == null) return; + final UsageCodeSnippetComponent codeSnippet = UsageCodeSnippetComponent.createUsageCodeSnippet(info); + Disposer.register(this, codeSnippet); + Color color = codeSnippet.getEditor().getBackgroundColor(); + add(getHeaderPanelForUsage(myOriginalUsage, info, color)); + add(codeSnippet); + } + + public void renderOriginalUsage() { + renderUsage(myOriginalUsage); + } + + public static @NotNull JPanel getHeaderPanelForUsage(@NotNull UsageInfo originalUsage, + @NotNull UsageInfo usageInfo, @NotNull Color backGroundColor) { + final JPanel header = new JPanel(new FlowLayout(FlowLayout.LEFT)); + header.setBackground(backGroundColor); + final JComponent link = new LocationLinkComponent(usageInfo).getComponent(); + header.add(link); + if (usageInfo == originalUsage) { + final SimpleColoredComponent component = new SimpleColoredComponent(); + component.append(UsageViewBundle.message("label.the.original.usage"), SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES); + header.add(component); + } + final Color color = new JBColor(Gray.xCD, Gray.x51); + header.setBorder(JBUI.Borders.customLineTop(color)); + return header; + } + + @Override + public void dispose() { + } +} diff --git a/platform/usageView-impl/src/com/intellij/find/findUsages/similarity/SimilarUsagesContextPanel.java b/platform/usageView-impl/src/com/intellij/find/findUsages/similarity/SimilarUsagesContextPanel.java new file mode 100644 index 000000000000..1df9bda4ee27 --- /dev/null +++ b/platform/usageView-impl/src/com/intellij/find/findUsages/similarity/SimilarUsagesContextPanel.java @@ -0,0 +1,131 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.find.findUsages.similarity; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.Ref; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.ui.ScrollPaneFactory; +import com.intellij.ui.components.JBPanelWithEmptyText; +import com.intellij.ui.components.panels.VerticalLayout; +import com.intellij.usageView.UsageInfo; +import com.intellij.usageView.UsageViewBundle; +import com.intellij.usages.PsiElementUsageTarget; +import com.intellij.usages.UsageContextPanel; +import com.intellij.usages.UsageTarget; +import com.intellij.usages.UsageView; +import com.intellij.usages.impl.UsageContextPanelBase; +import com.intellij.usages.impl.UsageViewImpl; +import com.intellij.usages.similarity.clustering.ClusteringSearchSession; +import com.intellij.usages.similarity.clustering.UsageCluster; +import com.intellij.usages.similarity.features.UsageSimilarityFeaturesProvider; +import com.intellij.util.RunnableCallable; +import com.intellij.util.concurrency.AppExecutorUtil; +import com.intellij.util.ui.scroll.BoundedRangeModelThresholdListener; +import kotlin.Unit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.util.List; + +import static com.intellij.find.findUsages.similarity.MostCommonUsagePatternsComponent.findClusteringSessionInUsageView; + +public class SimilarUsagesContextPanel extends UsageContextPanelBase { + + private @NotNull final UsageViewImpl myUsageView; + private @Nullable JScrollPane myScrollPane; + private @Nullable ClusteringSearchSession mySession; + private @Nullable SimilarUsagesComponent mySimilarUsagesComponent; + + public SimilarUsagesContextPanel(@NotNull Project project, + @NotNull UsageViewImpl usageView) { + super(project, usageView.getPresentation()); + myUsageView = usageView; + } + + @Override + public void dispose() { + super.dispose(); + myScrollPane = null; + if (mySimilarUsagesComponent != null) { + Disposer.dispose(mySimilarUsagesComponent); + } + mySimilarUsagesComponent = null; + } + + @Override + protected void updateLayoutLater(@Nullable List infos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + removeAll(); + if (mySimilarUsagesComponent != null) { + Disposer.dispose(mySimilarUsagesComponent); + } + JBPanelWithEmptyText mainPanel = new JBPanelWithEmptyText(); + add(mainPanel); + if (infos == null || infos.size() == 0) return; + UsageInfo info = infos.get(0); + Ref psiElementRef = new Ref<>(); + ReadAction.nonBlocking(new RunnableCallable(() -> psiElementRef.set(info.getElement()))).finishOnUiThread( + ModalityState.NON_MODAL, context -> { + PsiElement psiElement = psiElementRef.get(); + if (psiElement == null) return; + PsiFile psiFile = psiElement.getContainingFile(); + if (psiFile == null) return; + ClusteringSearchSession session = findSessionInUsages(); + if (session == null) return; + final UsageCluster cluster = session.findCluster(info); + if (cluster == null) return; + removeAll(); + mainPanel.setLayout(new VerticalLayout(0)); + mySimilarUsagesComponent = new SimilarUsagesComponent(info, this); + mainPanel.add(mySimilarUsagesComponent); + mySimilarUsagesComponent.renderOriginalUsage(); + mySimilarUsagesComponent.renderSimilarUsages(cluster.getUsages()); + myScrollPane = ScrollPaneFactory.createScrollPane(mainPanel); + myScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + BoundedRangeModelThresholdListener.Companion.install(myScrollPane.getVerticalScrollBar(), () -> { + mySimilarUsagesComponent.renderSimilarUsages(cluster.getUsages()); + return Unit.INSTANCE; + }); + add(myScrollPane, BorderLayout.CENTER); + }).expireWith(this).submit(AppExecutorUtil.getAppExecutorService()); + } + + private @Nullable ClusteringSearchSession findSessionInUsages() { + if (mySession == null) { + mySession = findClusteringSessionInUsageView(myUsageView); + } + return mySession; + } + + public static class Provider implements UsageContextPanel.Provider { + + @Override + public @NotNull UsageContextPanel create(@NotNull UsageView usageView) { + return new SimilarUsagesContextPanel(((UsageViewImpl)usageView).getProject(), (UsageViewImpl)usageView); + } + + @Override + public boolean isAvailableFor(@NotNull UsageView usageView) { + if (!ClusteringSearchSession.isSimilarUsagesClusteringEnabled()) return false; + UsageTarget[] targets = ((UsageViewImpl)usageView).getTargets(); + if (targets.length == 0) return false; + UsageTarget target = targets[0]; + if (!(target instanceof PsiElementUsageTarget)) return false; + return UsageSimilarityFeaturesProvider.EP_NAME.findFirstSafe(provider -> { + return provider.isAvailable((PsiElementUsageTarget)target); + }) != null; + } + + @Override + public @NotNull String getTabTitle() { + return UsageViewBundle.message("similar.usages.tab"); + } + } +} diff --git a/platform/usageView-impl/src/com/intellij/find/findUsages/similarity/UsageCodeSnippetComponent.java b/platform/usageView-impl/src/com/intellij/find/findUsages/similarity/UsageCodeSnippetComponent.java new file mode 100644 index 000000000000..cc10a3ead53d --- /dev/null +++ b/platform/usageView-impl/src/com/intellij/find/findUsages/similarity/UsageCodeSnippetComponent.java @@ -0,0 +1,85 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.find.findUsages.similarity; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorSettings; +import com.intellij.openapi.editor.LineNumberConverter; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.ex.EditorEx; +import com.intellij.openapi.editor.ex.MarkupModelEx; +import com.intellij.openapi.editor.markup.HighlighterLayer; +import com.intellij.openapi.editor.markup.HighlighterTargetArea; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.ui.EditorTextFieldCellRenderer; +import com.intellij.usageView.UsageInfo; +import com.intellij.util.ui.JBUI; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +public class UsageCodeSnippetComponent extends EditorTextFieldCellRenderer.SimpleWithGutterRendererComponent { + public static final int CONTEXT_LINE_NUMBER = 3; + + public UsageCodeSnippetComponent(@NotNull PsiElement element) { + super(element.getProject(), element.getLanguage(), false); + setupEditor(); + } + + private void setupEditor() { + EditorEx editor = getEditor(); + JScrollPane scrollPane = editor.getScrollPane(); + editor.setBorder(JBUI.Borders.empty()); + editor.getMarkupModel().removeAllHighlighters(); + editor.getGutterComponentEx().setPaintBackground(false); + scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); + scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + EditorSettings settings = editor.getSettings(); + settings.setLineNumbersShown(true); + settings.setAdditionalLinesCount(0); + settings.setLineCursorWidth(1); + settings.setDndEnabled(false); + settings.setUseSoftWraps(false); + } + + private void highlightRange(@NotNull TextRange rangeToHighlight) { + final MarkupModelEx markupModel = getEditor().getMarkupModel(); + markupModel.addRangeHighlighter(EditorColors.SEARCH_RESULT_ATTRIBUTES, rangeToHighlight.getStartOffset(), + rangeToHighlight.getEndOffset(), HighlighterLayer.ADDITIONAL_SYNTAX, HighlighterTargetArea.EXACT_RANGE); + } + + public void addUsagePreview(@NotNull UsageInfo usage) { + PsiElement element = usage.getElement(); + if (element == null) return; + PsiDocumentManager docManager = PsiDocumentManager.getInstance(element.getProject()); + Document doc = docManager.getDocument(element.getContainingFile()); + if (doc == null) return; + int usageStartLineNumber = doc.getLineNumber(usage.getElement().getTextRange().getStartOffset()); + int usageEndLineNumber = doc.getLineNumber(usage.getElement().getTextRange().getEndOffset()); + final int contextStartLineNumber = Math.max(0, usageStartLineNumber - CONTEXT_LINE_NUMBER); + int startOffset = doc.getLineStartOffset(contextStartLineNumber); + int endOffset = doc.getLineEndOffset(Math.min(usageEndLineNumber + CONTEXT_LINE_NUMBER, doc.getLineCount() - 1)); + getEditor().getGutterComponentEx().setLineNumberConverter( + contextStartLineNumber == 0 ? LineNumberConverter.DEFAULT : new LineNumberConverter.Increasing() { + @Override + public Integer convert(@NotNull Editor editor, int lineNumber) { + return lineNumber + contextStartLineNumber; + } + }); + setText(doc.getText(new TextRange(startOffset, endOffset))); + getEditor().getGutterComponentEx().updateUI(); + highlightRange(element.getTextRange().shiftLeft(startOffset)); + } + + public static @NotNull UsageCodeSnippetComponent createUsageCodeSnippet(@NotNull UsageInfo usage) { + PsiElement element = usage.getElement(); + assert element != null; + UsageCodeSnippetComponent usageCodeSnippetComponent = new UsageCodeSnippetComponent(element); + usageCodeSnippetComponent.addUsagePreview(usage); + return usageCodeSnippetComponent; + } +} + + diff --git a/platform/usageView-impl/src/com/intellij/usages/impl/UsageNodeTreeBuilder.java b/platform/usageView-impl/src/com/intellij/usages/impl/UsageNodeTreeBuilder.java index 8e77b6fd8d75..6c09d765a5d4 100644 --- a/platform/usageView-impl/src/com/intellij/usages/impl/UsageNodeTreeBuilder.java +++ b/platform/usageView-impl/src/com/intellij/usages/impl/UsageNodeTreeBuilder.java @@ -8,6 +8,7 @@ import com.intellij.usages.UsageGroup; import com.intellij.usages.UsageTarget; import com.intellij.usages.rules.UsageFilteringRule; import com.intellij.usages.rules.UsageGroupingRule; +import com.intellij.usages.similarity.usageAdapter.SimilarityUsage; import com.intellij.util.Consumer; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; @@ -58,6 +59,9 @@ class UsageNodeTreeBuilder { if (dumb && !DumbService.isDumbAware(rule)) continue; List groups = rule.getParentGroupsFor(usage, myTargets); + if (usage instanceof SimilarityUsage) { + ((SimilarityUsage)usage).addUsageGroupData(groups); + } for (UsageGroup group : groups) { groupNode = groupNode.addOrGetGroup(group, i, edtModelToSwingNodeChangesQueue); } diff --git a/platform/usageView-impl/src/com/intellij/usages/impl/UsagePreviewPanel.java b/platform/usageView-impl/src/com/intellij/usages/impl/UsagePreviewPanel.java index f1918e0d07da..5dcc2334a788 100644 --- a/platform/usageView-impl/src/com/intellij/usages/impl/UsagePreviewPanel.java +++ b/platform/usageView-impl/src/com/intellij/usages/impl/UsagePreviewPanel.java @@ -3,6 +3,7 @@ package com.intellij.usages.impl; import com.intellij.find.FindManager; import com.intellij.find.FindModel; +import com.intellij.find.findUsages.similarity.MostCommonUsagePatternsComponent; import com.intellij.ide.IdeTooltipManager; import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.openapi.actionSystem.CommonDataKeys; @@ -33,8 +34,10 @@ import com.intellij.ui.awt.RelativePoint; import com.intellij.usageView.UsageInfo; import com.intellij.usageView.UsageViewBundle; import com.intellij.usages.UsageContextPanel; +import com.intellij.usages.UsageGroup; import com.intellij.usages.UsageView; import com.intellij.usages.UsageViewPresentation; +import com.intellij.usages.similarity.clustering.ClusteringSearchSession; import com.intellij.util.ui.PositionTracker; import com.intellij.util.ui.StatusText; import org.jetbrains.annotations.NonNls; @@ -45,6 +48,7 @@ import javax.swing.*; import java.awt.*; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.regex.Pattern; @@ -60,6 +64,8 @@ public class UsagePreviewPanel extends UsageContextPanelBase implements DataProv private Pattern myCachedSearchPattern; private Pattern myCachedReplacePattern; private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private @Nullable UsageViewImpl myUsageView; + private @Nullable MostCommonUsagePatternsComponent myCommonUsagePatternsComponent; public UsagePreviewPanel(@NotNull Project project, @NotNull UsageViewPresentation presentation) { this(project, presentation, false); @@ -72,6 +78,16 @@ public class UsagePreviewPanel extends UsageContextPanelBase implements DataProv myIsEditor = isEditor; } + + public void setUsageView(@Nullable UsageViewImpl usageView) { + myUsageView = usageView; + } + + @Nullable + public UsageViewImpl getUsageView() { + return myUsageView; + } + @Nullable @Override public Object getData(@NotNull @NonNls String dataId) { @@ -88,11 +104,14 @@ public class UsagePreviewPanel extends UsageContextPanelBase implements DataProv return null; } + public static class Provider implements UsageContextPanel.Provider { @NotNull @Override public UsageContextPanel create(@NotNull UsageView usageView) { - return new UsagePreviewPanel(((UsageViewImpl)usageView).getProject(), usageView.getPresentation(), true); + UsagePreviewPanel previewPanel = new UsagePreviewPanel(((UsageViewImpl)usageView).getProject(), usageView.getPresentation(), true); + previewPanel.setUsageView((UsageViewImpl)usageView); + return previewPanel; } @Override @@ -326,6 +345,7 @@ public class UsagePreviewPanel extends UsageContextPanelBase implements DataProv public void dispose() { isDisposed = true; releaseEditor(); + disposeMostCommonUsageComponent(); for (Editor editor : EditorFactory.getInstance().getAllEditors()) { if (editor.getProject() == myProject && editor.getUserData(PREVIEW_EDITOR_FLAG) == this) { LOG.error("Editor was not released:" + editor); @@ -333,6 +353,13 @@ public class UsagePreviewPanel extends UsageContextPanelBase implements DataProv } } + private void disposeMostCommonUsageComponent() { + if (myCommonUsagePatternsComponent != null) { + Disposer.dispose(myCommonUsagePatternsComponent); + } + myCommonUsagePatternsComponent = null; + } + private void releaseEditor() { ApplicationManager.getApplication().assertIsDispatchThread(); if (myEditor != null) { @@ -371,8 +398,30 @@ public class UsagePreviewPanel extends UsageContextPanelBase implements DataProv } @Override - public void updateLayoutLater(@Nullable final List infos) { + public void updateLayoutLater(List infos, @NotNull Collection<@NotNull Collection> groups) { ApplicationManager.getApplication().assertIsDispatchThread(); + if (ClusteringSearchSession.isSimilarUsagesClusteringEnabled() && selectedGroupNodes(infos, groups) && myUsageView != null) { + releaseEditor(); + disposeMostCommonUsageComponent(); + removeAll(); + myCommonUsagePatternsComponent = new MostCommonUsagePatternsComponent(myUsageView, groups); + add(myCommonUsagePatternsComponent); + } else { + previewUsages(infos); + } + } + + private static boolean selectedGroupNodes(List infos, Collection> groups) { + return (infos == null || infos.isEmpty()) && !groups.isEmpty(); + } + + @Override + protected void updateLayoutLater(@Nullable List infos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + previewUsages(infos); + } + + private void previewUsages(@Nullable List infos) { String cannotPreviewMessage = cannotPreviewMessage(infos); if (cannotPreviewMessage == null) { resetEditor(infos); diff --git a/platform/usageView-impl/src/com/intellij/usages/impl/UsageViewImpl.java b/platform/usageView-impl/src/com/intellij/usages/impl/UsageViewImpl.java index 884f41fd3c72..d7c3a0d739b8 100644 --- a/platform/usageView-impl/src/com/intellij/usages/impl/UsageViewImpl.java +++ b/platform/usageView-impl/src/com/intellij/usages/impl/UsageViewImpl.java @@ -2230,7 +2230,7 @@ public class UsageViewImpl implements UsageViewEx { return USAGE_INFO_LIST_KEY.getData(DataManager.getInstance().getDataContext(myRootPanel)); } - private @NotNull Collection> getSelectedGroups() { + private @NotNull Collection<@NotNull Collection> getSelectedGroups() { ApplicationManager.getApplication().assertIsDispatchThread(); Collection> groupPaths = new ArrayList<>(); for (TreeNode node : selectedNodes()) { diff --git a/platform/usageView/resources/idea/UsageViewActions.xml b/platform/usageView/resources/idea/UsageViewActions.xml index db18880bc4f9..b3b7729535d4 100644 --- a/platform/usageView/resources/idea/UsageViewActions.xml +++ b/platform/usageView/resources/idea/UsageViewActions.xml @@ -9,6 +9,18 @@ + + + + + + + + diff --git a/platform/usageView/resources/messages/UsageViewBundle.properties b/platform/usageView/resources/messages/UsageViewBundle.properties index d933b5932242..7fa8ac00e901 100644 --- a/platform/usageView/resources/messages/UsageViewBundle.properties +++ b/platform/usageView/resources/messages/UsageViewBundle.properties @@ -11,6 +11,8 @@ action.UsageFiltering.WriteAccess.text=Show Write Access action.UsageFiltering.WriteAccess.description=Show usages that write the corresponding target action.UsageFiltering.GeneratedCode.text=Show Results In Generated Code action.UsageFiltering.Imports.text=Show Import Statements +0.similar.results.found.in.project={0, number} similar {0, choice, 1#result|2#results} found in project +similar.usages.tab=Similar Usages group.UsageGroupingActionGroup.text=Usage View Grouping action.group.by.title=Group By action.group.by.file.structure=File Structure @@ -127,4 +129,12 @@ usage.view.results.node.prefix=[{0}] {1} usage.view.results.node.scope.in=in references.to.be.changed=References to be changed {0} occurrences.found.in.comments.strings.and.non.java.files=Occurrences found in comments, strings and non-code files -occurrences.found.in.comments.strings.non.java.files.and.generated.code=Occurrences found in comments, strings, non-code files and generated code \ No newline at end of file +occurrences.found.in.comments.strings.non.java.files.and.generated.code=Occurrences found in comments, strings, non-code files and generated code +label.similar.usages=Similar usages +label.the.original.usage=(the original usage) +loading.most.common.usage.patterns=Loading most common usage patterns +show.0.similar.usages=Show {0, number} similar {0, choice, 1#usage|2#usages} +no.similar.usages.found=No Similar usages found +back=Back +refresh=Refresh +action.calculating.usage.features=Calculating Usage Features \ No newline at end of file diff --git a/platform/usageView/src/com/intellij/usages/UsageInfoToUsageConverter.java b/platform/usageView/src/com/intellij/usages/UsageInfoToUsageConverter.java index 95864f20d296..0aadf47cc8f2 100644 --- a/platform/usageView/src/com/intellij/usages/UsageInfoToUsageConverter.java +++ b/platform/usageView/src/com/intellij/usages/UsageInfoToUsageConverter.java @@ -7,6 +7,13 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.SmartPointerManager; import com.intellij.psi.SmartPsiElementPointer; import com.intellij.usageView.UsageInfo; +import com.intellij.usages.similarity.bag.Bag; +import com.intellij.usages.similarity.clustering.ClusteringSearchSession; +import com.intellij.usages.similarity.clustering.UsageCluster; +import com.intellij.usages.similarity.features.UsageSimilarityFeaturesProvider; +import com.intellij.usages.similarity.usageAdapter.SimilarityReadWriteUsageInfo2UsageAdapter; +import com.intellij.usages.similarity.usageAdapter.SimilarityUsage; +import com.intellij.usages.similarity.usageAdapter.SimilarityUsageInfo2UsageAdapter; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; @@ -109,6 +116,33 @@ public final class UsageInfoToUsageConverter { return new UsageInfo2UsageAdapter(usageInfo); } + + public static @NotNull Usage convertToSimilarUsage(PsiElement @NotNull [] primaryElements, + @NotNull UsageInfo usageInfo, + @NotNull ClusteringSearchSession session) { + PsiElement usageElement = usageInfo.getElement(); + if (usageElement != null && primaryElements.length != 0) { + Bag features = new Bag(); + UsageSimilarityFeaturesProvider.EP_NAME.forEachExtensionSafe(provider -> { + features.addAll(provider.getFeatures(usageElement)); + }); + final UsageCluster cluster = session.findOrCreateCluster(features); + if (cluster != null) { + final ReadWriteAccessDetector.Access readWriteAccess = ReadWriteUtil.getReadWriteAccess(primaryElements, usageElement); + final Usage similarityUsageAdapter; + if (readWriteAccess != null) { + similarityUsageAdapter = new SimilarityReadWriteUsageInfo2UsageAdapter(usageInfo, readWriteAccess, features, session, cluster); + } + else { + similarityUsageAdapter = new SimilarityUsageInfo2UsageAdapter(usageInfo, features, session, cluster); + } + cluster.addUsage((SimilarityUsage)similarityUsageAdapter); + return similarityUsageAdapter; + } + } + return convert(primaryElements, usageInfo); + } + public static Usage @NotNull [] convert(@NotNull TargetElementsDescriptor descriptor, UsageInfo @NotNull [] usageInfos) { Usage[] usages = new Usage[usageInfos.length]; for (int i = 0; i < usages.length; i++) { diff --git a/platform/usageView/src/com/intellij/usages/similarity/bag/Bag.java b/platform/usageView/src/com/intellij/usages/similarity/bag/Bag.java new file mode 100644 index 000000000000..76681802bc62 --- /dev/null +++ b/platform/usageView/src/com/intellij/usages/similarity/bag/Bag.java @@ -0,0 +1,86 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.usages.similarity.bag; + +import com.intellij.util.containers.Interner; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import org.jetbrains.annotations.NotNull; + +public class Bag { + + public static final Bag EMPTY_BAG = new Bag(); + private static final Interner ourInterner = Interner.createWeakInterner(); + private final @NotNull Object2IntMap myBag; + private int myCardinality; + + public Bag() { + myBag = new Object2IntOpenHashMap<>(); + myBag.defaultReturnValue(0); + } + + public Bag(String @NotNull ... words) { + myBag = new Object2IntOpenHashMap<>(); + myBag.defaultReturnValue(0); + for (String word : words) { + final String internedWord = ourInterner.intern(word); + myBag.put(internedWord, myBag.getInt(internedWord) + 1); + myCardinality += 1; + } + } + + public @NotNull Object2IntMap getBag() { + return myBag; + } + + public void addAll(@NotNull Bag bag) { + for (Object2IntMap.Entry entry : bag.myBag.object2IntEntrySet()) { + final String key = entry.getKey(); + myBag.put(key, myBag.getInt(key) + entry.getIntValue()); + myCardinality += entry.getIntValue(); + } + } + + public int getCardinality() { + return myCardinality; + } + + public void add(@NotNull String key) { + final String internedKey = ourInterner.intern(key); + myBag.put(internedKey, myBag.getInt(internedKey) + 1); + } + + public int get(@NotNull String word) { + return myBag.getInt(word); + } + + public boolean isEmpty() { + return myBag.isEmpty(); + } + + @Override + public String toString() { + StringBuilder toReturn = new StringBuilder(); + for (Object2IntMap.Entry entry : getBag().object2IntEntrySet()) { + toReturn.append(entry.getKey()).append(" : ").append(entry.getIntValue()).append("\n"); + } + return toReturn.toString(); + } + + public static int intersectionSize(@NotNull Bag bag1, @NotNull Bag bag2) { + Bag toIterate = (bag1.getBag().size() > bag2.getBag().size()) ? bag2 : bag1; + Bag toProcess = (bag1.getBag().size() > bag2.getBag().size()) ? bag1 : bag2; + int cardinality = 0; + for (Object2IntMap.Entry entry : toIterate.getBag().object2IntEntrySet()) { + cardinality += Math.min(toProcess.getBag().getInt(entry.getKey()), toIterate.getBag().getInt(entry.getKey())); + } + return cardinality; + } + + public static @NotNull Bag concat(Bag @NotNull ... bags) { + Bag bag = new Bag(); + for (Bag it : bags) { + bag.addAll(it); + } + return bag; + } +} diff --git a/platform/usageView/src/com/intellij/usages/similarity/bag/BagsDistanceCalculator.java b/platform/usageView/src/com/intellij/usages/similarity/bag/BagsDistanceCalculator.java new file mode 100644 index 000000000000..8a2901583cd4 --- /dev/null +++ b/platform/usageView/src/com/intellij/usages/similarity/bag/BagsDistanceCalculator.java @@ -0,0 +1,29 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.usages.similarity.bag; + +import org.jetbrains.annotations.NotNull; + +public class BagsDistanceCalculator { + private final double mySimilarityThreshold; + private final @NotNull Bag myExistingUsageFeatures; + + public BagsDistanceCalculator(@NotNull Bag existingUsageFeatures, double similarityThreshold) { + this.myExistingUsageFeatures = existingUsageFeatures; + this.mySimilarityThreshold = similarityThreshold; + } + + public double similarity(@NotNull Bag usageFeaturesToCheck) { + final double similarity = jaccardSimilarity(usageFeaturesToCheck, myExistingUsageFeatures); + if (similarity >= mySimilarityThreshold) { + return similarity; + } + return 0.0; + } + + public static double jaccardSimilarity(@NotNull Bag bag1, @NotNull Bag bag2) { + final int cardinality1 = bag1.getCardinality(); + final int cardinality2 = bag2.getCardinality(); + int intersectionSize = Bag.intersectionSize(bag1, bag2); + return intersectionSize / (double)(cardinality1 + cardinality2 - intersectionSize); + } +} diff --git a/platform/usageView/src/com/intellij/usages/similarity/clustering/ClusteringSearchSession.java b/platform/usageView/src/com/intellij/usages/similarity/clustering/ClusteringSearchSession.java new file mode 100644 index 000000000000..648ac936b34e --- /dev/null +++ b/platform/usageView/src/com/intellij/usages/similarity/clustering/ClusteringSearchSession.java @@ -0,0 +1,127 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.usages.similarity.clustering; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.util.registry.Registry; +import com.intellij.usageView.UsageInfo; +import com.intellij.usages.UsageGroup; +import com.intellij.usages.UsageInfo2UsageAdapter; +import com.intellij.usages.similarity.bag.Bag; +import com.intellij.usages.similarity.bag.BagsDistanceCalculator; +import com.intellij.usages.similarity.usageAdapter.SimilarityUsage; +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread; +import com.intellij.util.concurrency.annotations.RequiresReadLock; +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.CopyOnWriteArrayList; +import java.util.stream.Collectors; + + +/** + * Does usage clustering during the find usage process. Clusters are used on find usages results presentation. + */ +public class ClusteringSearchSession { + private final @NotNull List myClusters; + private final double mySimilarityThreshold; + + public ClusteringSearchSession() { + myClusters = new CopyOnWriteArrayList<>(); + mySimilarityThreshold = Registry.doubleValue("similarity.find.usages.groups.threshold"); + } + + public @NotNull List getClusters() { + return myClusters; + } + + @RequiresBackgroundThread + public synchronized @Nullable UsageCluster findOrCreateCluster(@NotNull Bag usageFeatures) { + if (usageFeatures.getBag().isEmpty()) { + return null; + } + UsageCluster cluster = getTheMostSimilarCluster(usageFeatures); + if (cluster == null) { + cluster = createNewCluster(); + } + return cluster; + } + + /** + * This method is designed to use from {@link com.intellij.usages.UsageContextPanel#updateLayout(List)} + */ + public @Nullable UsageCluster findCluster(@Nullable UsageInfo usageInfo) { + for (UsageCluster cluster : myClusters) { + for (SimilarityUsage usage : cluster.getUsages()) { + if (usage instanceof UsageInfo2UsageAdapter && ((UsageInfo2UsageAdapter)usage).getUsageInfo().equals(usageInfo)) { + return cluster; + } + } + } + return null; + } + + @RequiresBackgroundThread + @RequiresReadLock + public @NotNull List getClustersFilteredByGroups(@NotNull ProgressIndicator indicator, + @NotNull Collection> selectedGroups) { + //create new ArrayList from clusters to avoid a comparator contract violation during search + return new ArrayList<>(getClusters()).stream() + .sorted((o1, o2) -> { + indicator.checkCanceled(); + return Integer.compare(o2.getUsageFilteredByGroup(selectedGroups).size(), o1.getUsageFilteredByGroup(selectedGroups).size()); + }).collect(Collectors.toList()); + } + + private @NotNull UsageCluster createNewCluster() { + final UsageCluster newCluster = new UsageCluster(myClusters.size()); + myClusters.add(newCluster); + return newCluster; + } + + private @Nullable UsageCluster getTheMostSimilarCluster(@NotNull Bag features) { + if (myClusters.size() == 0) { + return null; + } + UsageCluster mostUsageCluster = null; + double maxSimilarity = Double.MIN_VALUE; + for (UsageCluster currentGroup : myClusters) { + double similarity = findMinimalSimilarity(currentGroup, features, mySimilarityThreshold); + if (maxSimilarity < similarity) { + if (similarity == 1.0) { + return currentGroup; + } + mostUsageCluster = currentGroup; + maxSimilarity = similarity; + } + } + return mostUsageCluster; + } + + private static double findMinimalSimilarity(@NotNull UsageCluster usageCluster, Bag newUsageFeatures, double threshold) { + final BagsDistanceCalculator bagsDistanceCalculator = new BagsDistanceCalculator(newUsageFeatures, threshold); + double min = Double.MAX_VALUE; + for (SimilarityUsage usage : usageCluster.getUsages()) { + final double similarity = bagsDistanceCalculator.similarity(usage.getFeatures()); + if (similarity < min) { + min = similarity; + } + if (min == 0.0) { + return min; + } + } + return min; + } + + @Nullable + public static ClusteringSearchSession createClusteringSessionIfEnabled() { + return isSimilarUsagesClusteringEnabled() ? (new ClusteringSearchSession()) : null; + } + + public static boolean isSimilarUsagesClusteringEnabled() { + return Registry.is("similarity.find.usages.enable") && ApplicationManager.getApplication().isInternal(); + } +} diff --git a/platform/usageView/src/com/intellij/usages/similarity/clustering/UsageCluster.java b/platform/usageView/src/com/intellij/usages/similarity/clustering/UsageCluster.java new file mode 100644 index 000000000000..98a78cd89c8d --- /dev/null +++ b/platform/usageView/src/com/intellij/usages/similarity/clustering/UsageCluster.java @@ -0,0 +1,64 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.usages.similarity.clustering; + +import com.intellij.usages.UsageGroup; +import com.intellij.usages.similarity.usageAdapter.SimilarityUsage; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.stream.Collectors; + +public class UsageCluster { + + private final int myIndex; + private final Set myUsages = new CopyOnWriteArraySet<>(); + + public UsageCluster(int index) { + this.myIndex = index; + } + + public int getIndex() { + return myIndex; + } + + public synchronized void addUsage(@NotNull SimilarityUsage usage) { + myUsages.add(usage); + } + + public @NotNull Set getUsages() { + return myUsages; + } + + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof UsageCluster)) return false; + + return myIndex == ((UsageCluster)o).myIndex; + } + + @NotNull + public Set getUsageFilteredByGroup(@NotNull Collection<@NotNull Collection> groups) { + return getUsages().stream().filter(e -> belongsToGroup(e, groups)).collect(Collectors.toSet()); + } + + public static boolean belongsToGroup(@NotNull SimilarityUsage info, + @NotNull Collection<@NotNull Collection> selectedGroupPaths) { + for (Collection selectedGroupPath : selectedGroupPaths) { + if (ContainerUtil.intersection(selectedGroupPath, info.getUsageGroupData()).size() == selectedGroupPath.size()) { + return true; + } + } + return false; + } + + @Override + public String toString() { + return "{" + + "id=" + myIndex + "\n" + + ", myUsages=" + myUsages.stream().map(usage -> usage.toString()).collect(Collectors.joining(",\n")) + + "}\n"; + } +} diff --git a/platform/usageView/src/com/intellij/usages/similarity/features/UsageSimilarityFeaturesProvider.java b/platform/usageView/src/com/intellij/usages/similarity/features/UsageSimilarityFeaturesProvider.java new file mode 100644 index 000000000000..e97f1d1c6b8d --- /dev/null +++ b/platform/usageView/src/com/intellij/usages/similarity/features/UsageSimilarityFeaturesProvider.java @@ -0,0 +1,24 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.usages.similarity.features; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.psi.PsiElement; +import com.intellij.usages.PsiElementUsageTarget; +import com.intellij.usages.similarity.bag.Bag; +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread; +import com.intellij.util.concurrency.annotations.RequiresReadLock; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Experimental +public interface UsageSimilarityFeaturesProvider { + @ApiStatus.Experimental + ExtensionPointName EP_NAME = ExtensionPointName.create("com.intellij.usageFeaturesProvider"); + + @RequiresReadLock + @RequiresBackgroundThread + @NotNull Bag getFeatures(@NotNull PsiElement usage); + + @RequiresReadLock + boolean isAvailable(@NotNull PsiElementUsageTarget usageTarget); +} diff --git a/platform/usageView/src/com/intellij/usages/similarity/features/UsageSimilarityFeaturesRecorder.java b/platform/usageView/src/com/intellij/usages/similarity/features/UsageSimilarityFeaturesRecorder.java new file mode 100644 index 000000000000..7019f875f28b --- /dev/null +++ b/platform/usageView/src/com/intellij/usages/similarity/features/UsageSimilarityFeaturesRecorder.java @@ -0,0 +1,104 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.usages.similarity.features; + +import com.intellij.openapi.util.registry.Registry; +import com.intellij.psi.PsiComment; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiWhiteSpace; +import com.intellij.psi.util.PsiUtilCore; +import com.intellij.usages.similarity.bag.Bag; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; + +public class UsageSimilarityFeaturesRecorder { + private final @NotNull Bag myFeatures; + private final @Nullable PsiElement myInitialElement; + + public UsageSimilarityFeaturesRecorder(@Nullable PsiElement initialContext) { + myFeatures = new Bag(); + myInitialElement = initialContext; + } + + public @NotNull Bag getFeatures() { + return myFeatures; + } + + public void addFeature(@NotNull String tokenFeature) { + myFeatures.add(tokenFeature); + } + + public void addAllFeatures(@NotNull PsiElement element, @Nullable String tokenFeature) { + if (tokenFeature != null) { + myFeatures.add(tokenFeature); + if (element != myInitialElement) { + addStructuralFeatures(element, tokenFeature); + } + } + } + + private void addParentFeatures(@NotNull PsiElement element, @Nullable String tokenFeature) { + if (!Registry.is("similarity.find.usages.use.parent.features")) { + return; + } + PsiElement parent = element.getParent(); + PsiElement grandParent; + PsiElement grandGrandParent; + if (parent != null) { + grandParent = parent.getParent(); + if (grandParent != null) { + myFeatures.add("P:" + tokenFeature + " " + PsiUtilCore.getElementType(parent) + " " + getChildNumber(parent, element)); + grandGrandParent = grandParent.getParent(); + if (grandGrandParent != null) { + myFeatures.add( + "GP:" + tokenFeature + " " + PsiUtilCore.getElementType(grandParent) + " " + getChildNumber(grandParent, parent)); + } + } + } + } + + private void addSiblingFeatures(@NotNull PsiElement element, @Nullable String tokenFeature) { + if (!Registry.is("similarity.find.usages.use.sibling.features")) { + return; + } + myFeatures.add("PREV: " + getPrevMeaningfulSibling(element) + " " + tokenFeature); + myFeatures.add("NEXT: " + tokenFeature + " " + getNextMeaningfulSibling(element)); + } + + public void addStructuralFeatures(@NotNull PsiElement element, @NotNull String tokenFeature) { + addParentFeatures(element, tokenFeature); + addSiblingFeatures(element, tokenFeature); + } + + private static int getChildNumber(@NotNull PsiElement parent, @NotNull PsiElement child) { + if (!Registry.is("similarity.find.usages.use.parent.features.with.child.number")) return -1; + PsiElement[] children = Arrays.stream(parent.getChildren()).filter(e -> !(e instanceof PsiWhiteSpace)).toArray(PsiElement[]::new); + for (int i = 0; i < children.length; i++) { + if (children[i] == child) { + return i; + } + } + return -1; + } + + private static @Nullable PsiElement getNextMeaningfulSibling(@NotNull PsiElement element) { + PsiElement nextSibling = element.getNextSibling(); + if (nextSibling != null) { + while (nextSibling instanceof PsiWhiteSpace || nextSibling instanceof PsiComment) { + nextSibling = nextSibling.getNextSibling(); + } + } + return nextSibling; + } + + private static @Nullable PsiElement getPrevMeaningfulSibling(@NotNull PsiElement element) { + PsiElement prevSibling = element.getPrevSibling(); + if (prevSibling != null) { + while (prevSibling instanceof PsiWhiteSpace || prevSibling instanceof PsiComment) { + prevSibling = prevSibling.getPrevSibling(); + } + } + return prevSibling; + } +} diff --git a/platform/usageView/src/com/intellij/usages/similarity/internal/ShowUsageFeaturesInternalAction.java b/platform/usageView/src/com/intellij/usages/similarity/internal/ShowUsageFeaturesInternalAction.java new file mode 100644 index 000000000000..4bdc303ba85a --- /dev/null +++ b/platform/usageView/src/com/intellij/usages/similarity/internal/ShowUsageFeaturesInternalAction.java @@ -0,0 +1,80 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.usages.similarity.internal; + +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileTypes.PlainTextFileType; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectUtil; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.*; +import com.intellij.usageView.UsageViewBundle; +import com.intellij.usages.similarity.bag.Bag; +import com.intellij.usages.similarity.features.UsageSimilarityFeaturesProvider; +import com.intellij.util.LocalTimeCounter; +import org.jetbrains.annotations.NotNull; + +import static com.intellij.openapi.application.ActionsKt.runWriteAction; + +public class ShowUsageFeaturesInternalAction extends AnAction { + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + Editor editor = e.getData(CommonDataKeys.EDITOR); + if (editor == null) return; + final PsiFile file = e.getData(CommonDataKeys.PSI_FILE); + if (file == null) return; + final Project project = e.getProject(); + assert project != null; + final VirtualFile projectDir = ProjectUtil.guessProjectDir(project); + assert projectDir != null; + PsiDirectory directory = PsiManager.getInstance(project).findDirectory(projectDir); + assert directory != null; + final Ref featuresDump = new Ref<>(); + calculateFeaturesForUsage(editor, file, project, featuresDump); + createFileWithFeatures(directory, featuresDump).navigate(true); + } + + private static void calculateFeaturesForUsage(@NotNull Editor editor, + @NotNull PsiFile file, + @NotNull Project project, + @NotNull Ref featuresDump) { + ProgressManager.getInstance().run(new Task.Modal(project, UsageViewBundle.message("action.calculating.usage.features"), true) { + @Override + public void run(@NotNull ProgressIndicator indicator) { + final Bag features = new Bag(); + ApplicationManager.getApplication().runReadAction(() -> { + PsiElement element = file.findElementAt(editor.getCaretModel().getOffset()); + if (element != null) { + UsageSimilarityFeaturesProvider.EP_NAME.forEachExtensionSafe(provider -> { + features.addAll(provider.getFeatures(element)); + }); + } + }); + featuresDump.set( + PsiFileFactory.getInstance(project).createFileFromText("features" + System.currentTimeMillis(), PlainTextFileType.INSTANCE, + StringUtil.join( + features.getBag().object2IntEntrySet(), ",\n"), + LocalTimeCounter.currentTime(), false, false)); + } + }); + } + + private static @NotNull PsiFile createFileWithFeatures(@NotNull PsiDirectory directory, @NotNull Ref featuresDumpFile) { + return runWriteAction(() -> + { + final PsiElement featureDumpFile = directory.add(featuresDumpFile.get()); + assert featureDumpFile instanceof PsiFile; + return (PsiFile)featureDumpFile; + } + ); + } +} diff --git a/platform/usageView/src/com/intellij/usages/similarity/usageAdapter/SimilarityReadWriteUsageInfo2UsageAdapter.java b/platform/usageView/src/com/intellij/usages/similarity/usageAdapter/SimilarityReadWriteUsageInfo2UsageAdapter.java new file mode 100644 index 000000000000..29dac3181f40 --- /dev/null +++ b/platform/usageView/src/com/intellij/usages/similarity/usageAdapter/SimilarityReadWriteUsageInfo2UsageAdapter.java @@ -0,0 +1,59 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.usages.similarity.usageAdapter; + +import com.intellij.codeInsight.highlighting.ReadWriteAccessDetector; +import com.intellij.usageView.UsageInfo; +import com.intellij.usages.ReadWriteAccessUsageInfo2UsageAdapter; +import com.intellij.usages.UsageGroup; +import com.intellij.usages.similarity.bag.Bag; +import com.intellij.usages.similarity.clustering.ClusteringSearchSession; +import com.intellij.usages.similarity.clustering.UsageCluster; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class SimilarityReadWriteUsageInfo2UsageAdapter extends ReadWriteAccessUsageInfo2UsageAdapter implements SimilarityUsage { + private final @NotNull Bag myFeatures; + private final @NotNull ClusteringSearchSession mySession; + + private final @Nullable UsageCluster myCluster; + private final @NotNull List myGroups; + + public SimilarityReadWriteUsageInfo2UsageAdapter(@NotNull UsageInfo usageInfo, + @NotNull ReadWriteAccessDetector.Access rwAccess, + @NotNull Bag features, + @NotNull ClusteringSearchSession session, @Nullable UsageCluster cluster) { + super(usageInfo, rwAccess); + myFeatures = features; + mySession = session; + myGroups = new ArrayList<>(); + myCluster = cluster; + } + + @Override + public @NotNull Bag getFeatures() { + return myFeatures; + } + + @Override + public @NotNull ClusteringSearchSession getClusteringSession() { + return mySession; + } + + @Override + public @Nullable UsageCluster getCluster() { + return myCluster; + } + + @Override + public @NotNull List getUsageGroupData() { + return myGroups; + } + + @Override + public void addUsageGroupData(@NotNull List groups) { + myGroups.addAll(groups); + } +} diff --git a/platform/usageView/src/com/intellij/usages/similarity/usageAdapter/SimilarityUsage.java b/platform/usageView/src/com/intellij/usages/similarity/usageAdapter/SimilarityUsage.java new file mode 100644 index 000000000000..d350495968d3 --- /dev/null +++ b/platform/usageView/src/com/intellij/usages/similarity/usageAdapter/SimilarityUsage.java @@ -0,0 +1,44 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.usages.similarity.usageAdapter; + +import com.intellij.usages.UsageGroup; +import com.intellij.usages.similarity.bag.Bag; +import com.intellij.usages.similarity.clustering.ClusteringSearchSession; +import com.intellij.usages.similarity.clustering.UsageCluster; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * This interface marks UsageInfo2UsageAdapter implementation as a subject of clustering + */ +@ApiStatus.Experimental +public interface SimilarityUsage { + + /** + * Each usage provides a {@link Bag} of features (string tokens) collected by {@link com.intellij.usages.similarity.features.UsageSimilarityFeaturesProvider} implementation + * + * @return a Bag of words {@link Bag} with string features related to usage context usage + */ + @NotNull Bag getFeatures(); + + @NotNull ClusteringSearchSession getClusteringSession(); + + @Nullable UsageCluster getCluster(); + + /** + * Pre-cached {@link UsageGroup} data from {@link com.intellij.usages.rules.UsageGroupingRule} which is calculated on Usage tree building + * + * @return list of usage {@link UsageGroup} obtained from {@link com.intellij.usages.rules.UsageGroupingRule} + */ + @NotNull List getUsageGroupData(); + + /** + * This method is use for adding group data + * + * @param groups + */ + void addUsageGroupData(@NotNull List groups); +} diff --git a/platform/usageView/src/com/intellij/usages/similarity/usageAdapter/SimilarityUsageInfo2UsageAdapter.java b/platform/usageView/src/com/intellij/usages/similarity/usageAdapter/SimilarityUsageInfo2UsageAdapter.java new file mode 100644 index 000000000000..e1122f1b7002 --- /dev/null +++ b/platform/usageView/src/com/intellij/usages/similarity/usageAdapter/SimilarityUsageInfo2UsageAdapter.java @@ -0,0 +1,56 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.usages.similarity.usageAdapter; + +import com.intellij.usageView.UsageInfo; +import com.intellij.usages.UsageGroup; +import com.intellij.usages.UsageInfo2UsageAdapter; +import com.intellij.usages.similarity.bag.Bag; +import com.intellij.usages.similarity.clustering.ClusteringSearchSession; +import com.intellij.usages.similarity.clustering.UsageCluster; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class SimilarityUsageInfo2UsageAdapter extends UsageInfo2UsageAdapter implements SimilarityUsage { + + private final @NotNull Bag myFeatures; + private final @NotNull ClusteringSearchSession mySession; + private final @NotNull List myGroups; + private final @Nullable UsageCluster myCluster; + + public SimilarityUsageInfo2UsageAdapter(@NotNull UsageInfo usageInfo, @NotNull Bag features, @NotNull ClusteringSearchSession session, + @Nullable UsageCluster cluster) { + super(usageInfo); + myFeatures = features; + mySession = session; + myGroups = new ArrayList<>(); + myCluster = cluster; + } + + @Override + public @NotNull ClusteringSearchSession getClusteringSession() { + return mySession; + } + + @Override + public @Nullable UsageCluster getCluster() { + return myCluster; + } + + @Override + public @NotNull List getUsageGroupData() { + return myGroups; + } + + @Override + public void addUsageGroupData(@NotNull List groups) { + myGroups.addAll(groups); + } + + @Override + public @NotNull Bag getFeatures() { + return myFeatures; + } +} diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/findUsages/similarity/KotlinSimilarityPropertiesExtractor.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/findUsages/similarity/KotlinSimilarityPropertiesExtractor.kt new file mode 100644 index 000000000000..b8b014211400 --- /dev/null +++ b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/findUsages/similarity/KotlinSimilarityPropertiesExtractor.kt @@ -0,0 +1,63 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + +package org.jetbrains.kotlin.idea.findUsages.similarity + +import com.intellij.psi.PsiElement +import com.intellij.usages.similarity.bag.Bag +import com.intellij.usages.similarity.features.UsageSimilarityFeaturesRecorder +import org.jetbrains.kotlin.psi.* + +class KotlinSimilarityPropertiesExtractor(context: PsiElement) : KtTreeVisitorVoid() { + private val myUsageSimilarityFeaturesRecorder = + UsageSimilarityFeaturesRecorder(context) + private val myContext = context + + fun getFeatures(): Bag { + myContext.accept(this) + return myUsageSimilarityFeaturesRecorder.features + } + + override fun visitStringTemplateExpression(expression: KtStringTemplateExpression) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(expression, "literal: string") + super.visitStringTemplateExpression(expression) + } + + override fun visitProperty(property: KtProperty) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(property, "VAR: ") + super.visitProperty(property) + } + + override fun visitBinaryExpression(expression: KtBinaryExpression) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(expression, expression.operationToken.toString()) + super.visitBinaryExpression(expression) + } + + override fun visitIsExpression(expression: KtIsExpression) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(expression, "instanceof") + super.visitIsExpression(expression) + } + + override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(lambdaExpression, "lambda") + super.visitLambdaExpression(lambdaExpression) + } + + override fun visitUnaryExpression(expression: KtUnaryExpression) { + myUsageSimilarityFeaturesRecorder.addAllFeatures(expression, expression.operationToken.toString()) + super.visitUnaryExpression(expression) + } + + override fun visitReferenceExpression(expression: KtReferenceExpression) { + var feature = "VAR:" + if (!theFirstReferenceInQualifiedExpression(expression)) { + if (expression is KtNameReferenceExpression) { + feature = "{CALL: ${expression.getReferencedName()}}" + } + } + myUsageSimilarityFeaturesRecorder.addAllFeatures(expression, feature) + super.visitReferenceExpression(expression) + } + + private fun theFirstReferenceInQualifiedExpression(expression: KtReferenceExpression) = + (expression.parent is KtQualifiedExpression && expression.parent.firstChild == expression) +} \ No newline at end of file diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/findUsages/similarity/KotlinUsageSimilarityFeaturesProvider.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/findUsages/similarity/KotlinUsageSimilarityFeaturesProvider.kt new file mode 100644 index 000000000000..15269a6cada7 --- /dev/null +++ b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/findUsages/similarity/KotlinUsageSimilarityFeaturesProvider.kt @@ -0,0 +1,46 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + +package org.jetbrains.kotlin.idea.findUsages.similarity + +import com.intellij.openapi.util.Condition +import com.intellij.openapi.util.registry.Registry +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.usages.PsiElementUsageTarget +import com.intellij.usages.similarity.bag.Bag +import com.intellij.usages.similarity.features.UsageSimilarityFeaturesProvider +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread +import com.intellij.util.concurrency.annotations.RequiresReadLock +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtFunction +import org.jetbrains.kotlin.psi.KtStatementExpression + +class KotlinUsageSimilarityFeaturesProvider : UsageSimilarityFeaturesProvider { + @RequiresReadLock + @RequiresBackgroundThread + override fun getFeatures(usage: PsiElement): Bag { + val properties = Bag() + if (!Registry.`is`("similarity.find.usages.kotlin.clustering.enable")) { + return properties + } + val statement = getContext(usage) + if (statement != null) { + properties.addAll(KotlinSimilarityPropertiesExtractor(statement).getFeatures()) + } + + return properties + } + + @RequiresReadLock + override fun isAvailable(usageTarget: PsiElementUsageTarget): Boolean { + return usageTarget.element is KtFunction + } + + fun getContext(element: PsiElement): PsiElement? { + return PsiTreeUtil.findFirstParent( + element, + false, + Condition { e: PsiElement? -> e is KtStatementExpression || e?.parent is KtBlockExpression }, + ) + } +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/findUsages/KotlinGroupUsagesBySimilarityTest.kt b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/findUsages/KotlinGroupUsagesBySimilarityTest.kt new file mode 100644 index 000000000000..ea1b619e7706 --- /dev/null +++ b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/findUsages/KotlinGroupUsagesBySimilarityTest.kt @@ -0,0 +1,64 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.kotlin.findUsages + +import com.intellij.testFramework.TestDataPath +import com.intellij.testFramework.assertEqualsToFile +import com.intellij.usages.UsageInfoToUsageConverter +import com.intellij.usages.similarity.clustering.ClusteringSearchSession +import org.jetbrains.kotlin.idea.findUsages.similarity.KotlinUsageSimilarityFeaturesProvider +import org.jetbrains.kotlin.idea.test.TestRoot +import org.jetbrains.kotlin.test.TestMetadata +import org.junit.internal.runners.JUnit38ClassRunner +import org.junit.runner.RunWith +import java.io.File + +@TestRoot("idea/tests") +@TestDataPath("\$CONTENT_ROOT") +@TestMetadata("testData/findUsages/similarity") +@RunWith( + JUnit38ClassRunner::class +) +class KotlinGroupUsagesBySimilarityTest : AbstractFindUsagesTest() { + fun testSimpleFeatures() { + myFixture.configureByFile(getTestName(true)+".kt") + val elementAtCaret = myFixture.getReferenceAtCaretPosition()!!.element + val features = KotlinUsageSimilarityFeaturesProvider().getFeatures(elementAtCaret) + assertEquals(1, features["VAR: "]) + assertEquals(1, features["{CALL: inc}"]) + } + + @TestMetadata("general.kt") + @Throws(Exception::class) + fun testGeneral() { + doTest() + } + + @TestMetadata("chainCalls") + fun testChainCalls() { + myFixture.configureByFile(getTestName(true)+".kt") + val findUsages = findUsages(myFixture.elementAtCaret, null, false, myFixture.project) + val session = ClusteringSearchSession(); + findUsages.forEach { usage -> UsageInfoToUsageConverter.convertToSimilarUsage(arrayOf(myFixture.elementAtCaret), usage, session) } + val file = File(testDataDirectory, getTestName(true)+".results.txt") + assertEqualsToFile("", file, session.clusters.toString()) + } + + fun testListAdd() { + doTest() + } + + fun testFilter() { + doTest() + } + + private fun doTest() { + myFixture.configureByFile(getTestName(true)+".kt") + val findUsages = findUsages(myFixture.elementAtCaret, null, false, myFixture.project) + val session = ClusteringSearchSession(); + findUsages.forEach { usage -> UsageInfoToUsageConverter.convertToSimilarUsage(arrayOf(myFixture.elementAtCaret), usage, session) } + val file = File(testDataDirectory, getTestName(true) + ".results.txt") + assertEqualsToFile("", file, session.clusters.toString()) + } + +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/findUsages/similarity/chainCalls.kt b/plugins/kotlin/idea/tests/testData/findUsages/similarity/chainCalls.kt new file mode 100644 index 000000000000..1b56ab3b83f4 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/findUsages/similarity/chainCalls.kt @@ -0,0 +1,39 @@ +class PsiElement() { + + val operationReference = PsiElement() + lateinit var myTextRange: TextRange + + public constructor(textRange: TextRange) : this() { + myTextRange = textRange; + } + + public fun getTextRange(): TextRange { + return myTextRange; + } +} + +class TextRange(val startOffset: Int, val endOffset: Int) { + + fun containsOffset(caretOffset: Int): Boolean { + + } + +} + + +class ToTest { + fun f() { + val element = PsiElement(TextRange(1, 5)) + val opRef = element.operationReference + if (!opRef.getTextRange().containsOffset(2)) return + + System.out.println(element.getTextRange()) + System.out.println(opRef.getTextRange().containsOffset(2)) + System.out.println(opRef.getTextRange().containsOffset(5)) + System.out.println(element.getTextRange().startOffset + 1) + System.out.println(element.getTextRange().startOffset + 2) + if (!opRef.getTextRange().containsOffset(3)) return + + + } +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/findUsages/similarity/chainCalls.results.txt b/plugins/kotlin/idea/tests/testData/findUsages/similarity/chainCalls.results.txt new file mode 100644 index 000000000000..bc6b5d33b324 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/findUsages/similarity/chainCalls.results.txt @@ -0,0 +1,12 @@ +[{id=0 +, myUsages=28|if| (!opRef.|getTextRange|().containsOffset(|2|)) |return, +35|if| (!opRef.|getTextRange|().containsOffset(|3|)) |return} +, {id=1 +, myUsages=30|System.out.println(element.|getTextRange|())} +, {id=2 +, myUsages=31|System.out.println(opRef.|getTextRange|().containsOffset(|2|)), +32|System.out.println(opRef.|getTextRange|().containsOffset(|5|))} +, {id=3 +, myUsages=33|System.out.println(element.|getTextRange|().startOffset + |1|), +34|System.out.println(element.|getTextRange|().startOffset + |2|)} +] diff --git a/plugins/kotlin/idea/tests/testData/findUsages/similarity/filter.kt b/plugins/kotlin/idea/tests/testData/findUsages/similarity/filter.kt new file mode 100644 index 000000000000..a9f17721af6b --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/findUsages/similarity/filter.kt @@ -0,0 +1,12 @@ +fun f() { + val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11) + + var filteredMap = ""; + filteredMap = numbersMap.filter { (key, value) -> value > 10 && key.endsWith("1")} + filteredMap = numbersMap.filter { (key, value) -> key.endsWith("5") && value > 1} + filteredMap = numbersMap.filter { (key, value) -> false } + filteredMap = numbersMap.filter { (key, value) -> true } + filteredMap = numbersMap.filter { (key, value) -> key.endsWith("1") } + filteredMap = numbersMap.filter { (key, value) -> key.endsWith("1") } + println(filteredMap) +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/findUsages/similarity/filter.results.txt b/plugins/kotlin/idea/tests/testData/findUsages/similarity/filter.results.txt new file mode 100644 index 000000000000..65ee1e5b560d --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/findUsages/similarity/filter.results.txt @@ -0,0 +1,10 @@ +[{id=0 +, myUsages=5|filteredMap = numbersMap.|filter| { (key, value) -> value > |10| && key.endsWith(|"1"|)}, +6|filteredMap = numbersMap.|filter| { (key, value) -> key.endsWith(|"5"|) && value > |1|}} +, {id=1 +, myUsages=7|filteredMap = numbersMap.|filter| { (key, value) -> |false| }, +8|filteredMap = numbersMap.|filter| { (key, value) -> |true| }} +, {id=2 +, myUsages=9|filteredMap = numbersMap.|filter| { (key, value) -> key.endsWith(|"1"|) }, +10|filteredMap = numbersMap.|filter| { (key, value) -> key.endsWith(|"1"|) }} +] diff --git a/plugins/kotlin/idea/tests/testData/findUsages/similarity/general.kt b/plugins/kotlin/idea/tests/testData/findUsages/similarity/general.kt new file mode 100644 index 000000000000..2c11bae186c5 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/findUsages/similarity/general.kt @@ -0,0 +1,22 @@ +import java.lang.System + +// PSI_ELEMENT: org.jetbrains.kotlin.psi.KtNamedFunction +// OPTIONS: usages + +class Foo { + fun getFive(){ + return 5 + } + fun off(a){ + + } + + fun m() { + java.lang.System.out.println(off(1)) + off(1) + off(1) + if (off(getFive())) { + + } + } +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/findUsages/similarity/general.results.txt b/plugins/kotlin/idea/tests/testData/findUsages/similarity/general.results.txt new file mode 100644 index 000000000000..90ede79f4e51 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/findUsages/similarity/general.results.txt @@ -0,0 +1,8 @@ +[{id=0 +, myUsages=15|java.lang.System.out.println(|off|(|1|))} +, {id=1 +, myUsages=16|off|(|1|), +17|off|(|1|)} +, {id=2 +, myUsages=18|if| (|off|(getFive())) {} +] diff --git a/plugins/kotlin/idea/tests/testData/findUsages/similarity/listAdd.kt b/plugins/kotlin/idea/tests/testData/findUsages/similarity/listAdd.kt new file mode 100644 index 000000000000..e68f4a6afd15 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/findUsages/similarity/listAdd.kt @@ -0,0 +1,8 @@ +fun f() { + val numbers = mutableListOf("one", "two", "three", "four") + numbers.add ("five") + numbers.add("zero") + numbers.add(getStr()) + numbers.add(getStr() + "a") + numbers.add(getStr() + "b") +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/findUsages/similarity/listAdd.results.txt b/plugins/kotlin/idea/tests/testData/findUsages/similarity/listAdd.results.txt new file mode 100644 index 000000000000..1e56d6f21683 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/findUsages/similarity/listAdd.results.txt @@ -0,0 +1,9 @@ +[{id=0 +, myUsages=3|numbers.|add| (|"five"|), +4|numbers.|add|(|"zero"|)} +, {id=1 +, myUsages=5|numbers.|add|(getStr())} +, {id=2 +, myUsages=6|numbers.|add|(getStr() + |"a"|), +7|numbers.|add|(getStr() + |"b"|)} +] diff --git a/plugins/kotlin/idea/tests/testData/findUsages/similarity/simpleFeatures.kt b/plugins/kotlin/idea/tests/testData/findUsages/similarity/simpleFeatures.kt new file mode 100644 index 000000000000..58b4222b4e9b --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/findUsages/similarity/simpleFeatures.kt @@ -0,0 +1,2 @@ +var a = Char() +var length = a.inc() \ No newline at end of file diff --git a/plugins/kotlin/plugin/resources/META-INF/kotlin-core.xml b/plugins/kotlin/plugin/resources/META-INF/kotlin-core.xml index b25aa2eb09fd..74bc9539722d 100644 --- a/plugins/kotlin/plugin/resources/META-INF/kotlin-core.xml +++ b/plugins/kotlin/plugin/resources/META-INF/kotlin-core.xml @@ -381,6 +381,8 @@ + +