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 extends UsageGroup>> 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 extends UsageInfo> 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 extends UsageInfo> infos) {
+ public void updateLayoutLater(List extends UsageInfo> infos, @NotNull Collection<@NotNull Collection extends UsageGroup>> 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 extends UsageInfo> infos, Collection> groups) {
+ return (infos == null || infos.isEmpty()) && !groups.isEmpty();
+ }
+
+ @Override
+ protected void updateLayoutLater(@Nullable List extends UsageInfo> infos) {
+ ApplicationManager.getApplication().assertIsDispatchThread();
+ previewUsages(infos);
+ }
+
+ private void previewUsages(@Nullable List extends UsageInfo> 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 extends UsageGroup>> 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 extends UsageGroup>> groups) {
+ return getUsages().stream().filter(e -> belongsToGroup(e, groups)).collect(Collectors.toSet());
+ }
+
+ public static boolean belongsToGroup(@NotNull SimilarityUsage info,
+ @NotNull Collection<@NotNull Collection extends UsageGroup>> selectedGroupPaths) {
+ for (Collection extends UsageGroup> 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 @@
+
+