From c4272dc602ce6f233ea6cddf705452d029fd9544 Mon Sep 17 00:00:00 2001 From: Marcin Mikosik Date: Thu, 11 Sep 2025 08:47:24 +0000 Subject: [PATCH] [java] IDEA-216276 Add ShowDuplicateElementsFix that shows popup with a list of element duplicates and use it for duplicate-method error #IDEA-216276 Fixed Merge-request: IJ-MR-171237 Merged-by: Marcin Mikosik GitOrigin-RevId: 5aab229888a361d3d4f653708b7f42d832c44645 --- .../intention/QuickFixFactory.java | 2 + .../messages/QuickFixBundle.properties | 5 + .../analysis/DefaultJavaErrorFixProvider.java | 10 +- .../NavigateToDuplicateElementFix.java | 4 + .../quickfix/ShowDuplicateElementsFix.java | 119 ++++++++++++++++++ .../impl/config/QuickFixFactoryImpl.java | 5 + .../afterNavigatesFromFirstToSecond.java | 2 +- ...vigatesFromFirstToSecondGenericMethod.java | 2 +- .../afterNavigatesFromThirdToFirst.java | 2 +- ...vigatesToEnumValueOfWhenDeclaredTwice.java | 2 +- ...avigatesToEnumValuesWhenDeclaredTwice.java | 2 +- .../beforeDoesntNavigateToEnumValueOf.java | 2 +- .../beforeDoesntNavigateToEnumValues.java | 2 +- .../beforeNavigatesFromFirstToSecond.java | 2 +- ...vigatesFromFirstToSecondGenericMethod.java | 2 +- .../beforeNavigatesFromThirdToFirst.java | 2 +- ...vigatesToEnumValueOfWhenDeclaredTwice.java | 2 +- ...avigatesToEnumValuesWhenDeclaredTwice.java | 2 +- .../NavigateToDuplicateMethodFixTest.java | 11 -- .../quickfix/ShowDuplicateMethodsFixTest.java | 28 +++++ .../preview/IntentionPreviewDiffResultTest.kt | 17 ++- .../analysis-api/api-dump-experimental.txt | 11 ++ platform/analysis-api/api-dump.txt | 1 + .../preview/IntentionPreviewInfo.java | 38 +++++- platform/lang-impl/api-dump-unreviewed.txt | 2 + .../CommandCompletionDocumentationProvider.kt | 1 + .../preview/IntentionPreviewDiffResult.kt | 15 +++ .../IntentionPreviewPopupUpdateProcessor.kt | 1 + 28 files changed, 264 insertions(+), 30 deletions(-) create mode 100644 java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/ShowDuplicateElementsFix.java rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/{navigateToDuplicateMethodFix => showDuplicateMethodsFix}/afterNavigatesFromFirstToSecond.java (72%) rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/{navigateToDuplicateMethodFix => showDuplicateMethodsFix}/afterNavigatesFromFirstToSecondGenericMethod.java (76%) rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/{navigateToDuplicateMethodFix => showDuplicateMethodsFix}/afterNavigatesFromThirdToFirst.java (72%) rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/{navigateToDuplicateMethodFix => showDuplicateMethodsFix}/afterNavigatesToEnumValueOfWhenDeclaredTwice.java (77%) rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/{navigateToDuplicateMethodFix => showDuplicateMethodsFix}/afterNavigatesToEnumValuesWhenDeclaredTwice.java (75%) rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/{navigateToDuplicateMethodFix => showDuplicateMethodsFix}/beforeDoesntNavigateToEnumValueOf.java (67%) rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/{navigateToDuplicateMethodFix => showDuplicateMethodsFix}/beforeDoesntNavigateToEnumValues.java (66%) rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/{navigateToDuplicateMethodFix => showDuplicateMethodsFix}/beforeNavigatesFromFirstToSecond.java (68%) rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/{navigateToDuplicateMethodFix => showDuplicateMethodsFix}/beforeNavigatesFromFirstToSecondGenericMethod.java (73%) rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/{navigateToDuplicateMethodFix => showDuplicateMethodsFix}/beforeNavigatesFromThirdToFirst.java (68%) rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/{navigateToDuplicateMethodFix => showDuplicateMethodsFix}/beforeNavigatesToEnumValueOfWhenDeclaredTwice.java (74%) rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/{navigateToDuplicateMethodFix => showDuplicateMethodsFix}/beforeNavigatesToEnumValuesWhenDeclaredTwice.java (72%) delete mode 100644 java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/quickfix/NavigateToDuplicateMethodFixTest.java create mode 100644 java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/quickfix/ShowDuplicateMethodsFixTest.java diff --git a/java/java-analysis-api/src/com/intellij/codeInsight/intention/QuickFixFactory.java b/java/java-analysis-api/src/com/intellij/codeInsight/intention/QuickFixFactory.java index 3a4bd345108e..308551a257c1 100644 --- a/java/java-analysis-api/src/com/intellij/codeInsight/intention/QuickFixFactory.java +++ b/java/java-analysis-api/src/com/intellij/codeInsight/intention/QuickFixFactory.java @@ -127,6 +127,8 @@ public abstract class QuickFixFactory { public abstract @NotNull IntentionAction createNavigateToDuplicateElementFix(@NotNull NavigatablePsiElement element); + public abstract @NotNull IntentionAction createShowDuplicateElementsFix(@NotNull List<@NotNull ? extends NavigatablePsiElement> elements); + public abstract @NotNull IntentionAction createConvertToStringLiteralAction(); /** diff --git a/java/java-analysis-impl/resources/messages/QuickFixBundle.properties b/java/java-analysis-impl/resources/messages/QuickFixBundle.properties index afa0880cd45a..53d64444a695 100644 --- a/java/java-analysis-impl/resources/messages/QuickFixBundle.properties +++ b/java/java-analysis-impl/resources/messages/QuickFixBundle.properties @@ -209,6 +209,11 @@ reuse.variable.declaration.text=Reuse previous variable ''{0}'' navigate.variable.declaration.family=Navigate to variable declaration navigate.variable.declaration.text=Navigate to previous declared variable ''{0}'' navigate.duplicate.element.text=Navigate to duplicate {0} +show.duplicate.elements.family=Show duplicates +show.duplicate.elements.text=Show ''{0}'' duplicates +show.duplicate.elements.popup.title=Choose Duplicate +show.duplicate.elements.navigate.family=Navigate to duplicate +show.duplicate.elements.navigate.text=Line #{0} safe.delete.family=Safe delete safe.delete.text=Safe delete ''{0}'' setup.jdk.location.family=Setup JDK Location diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/DefaultJavaErrorFixProvider.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/DefaultJavaErrorFixProvider.java index 8a09197f5279..ef7894d4dd8e 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/DefaultJavaErrorFixProvider.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/DefaultJavaErrorFixProvider.java @@ -338,11 +338,11 @@ public final class DefaultJavaErrorFixProvider extends AbstractJavaErrorFixProvi error.context().method() instanceof SyntheticElement ? null : myFactory.createSameErasureButDifferentMethodsFix(error.context().method(), error.context().superMethod())); fixes(METHOD_DUPLICATE, (error, sink) -> { - error.context().methods().stream() - .filter(m -> !m.equals(error.psi())) - .filter(m -> !(m instanceof SyntheticElement)) // filters out synthetic methods, such as Enum#values() - .findFirst() - .ifPresent(m -> sink.accept(myFactory.createNavigateToDuplicateElementFix(m))); + // filters out synthetic methods, such as Enum#values() + var duplicates = ContainerUtil.filter(error.context().methods(), m -> !(m instanceof SyntheticElement)); + if (duplicates.size() > 1) { + sink.accept(myFactory.createShowDuplicateElementsFix(duplicates)); + } }); } diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/NavigateToDuplicateElementFix.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/NavigateToDuplicateElementFix.java index 545e881b46cf..ccc3971ca394 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/NavigateToDuplicateElementFix.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/NavigateToDuplicateElementFix.java @@ -27,6 +27,10 @@ public class NavigateToDuplicateElementFix extends PsiBasedModCommandAction { + private final List> myNavigatablePsiElements; + + public ShowDuplicateElementsFix(List duplicates) { + super(NavigatablePsiElement.class); + myNavigatablePsiElements = ContainerUtil.map(duplicates, dup -> SmartPointerManager.createPointer(dup)); + } + + @Override + public String getFamilyName() { + return QuickFixBundle.message("show.duplicate.elements.family"); + } + + @Override + protected Presentation getPresentation(ActionContext context, NavigatablePsiElement section) { + var name = QuickFixBundle.message("show.duplicate.elements.text", elementName(getDuplicatePsiElements())); + return Presentation.of(name); + } + + @Override + protected ModCommand perform(ActionContext context, NavigatablePsiElement element) { + var title = QuickFixBundle.message("show.duplicate.elements.popup.title"); + var navigateActions = ContainerUtil.map(getDuplicatePsiElements(), d -> navigateAction(d)); + return ModCommand.chooseAction(title, navigateActions); + } + + private static String elementName(List elements) { + if (!elements.isEmpty()) { + NavigatablePsiElement element = elements.get(0); + if (element instanceof PsiMethod method) { + return method.getName() + "()"; + } + return Objects.requireNonNullElse(element.getName(), ""); + } + return ""; + } + + @Override + protected IntentionPreviewInfo generatePreview(ActionContext context, NavigatablePsiElement element) { + var builder = new HtmlBuilder(); + var elements = getDuplicatePsiElements(); + for (int i = 0; i < elements.size(); i++) { + if (i != 0) { + builder.append(HtmlChunk.br()); + } + var current = elements.get(i); + builder.append(IntentionPreviewInfo.navigatePreviewHtmlChunk(current.getContainingFile(), current.getTextOffset())); + } + return new IntentionPreviewInfo.Html(builder.toFragment()); + } + + private List getDuplicatePsiElements() { + return ContainerUtil.mapNotNull(myNavigatablePsiElements, SmartPsiElementPointer::getElement); + } + + private static ModCommandAction navigateAction(NavigatablePsiElement navigatablePsiElement) { + return new NavigateToAction(navigatablePsiElement); + } + + private static class NavigateToAction extends PsiBasedModCommandAction { + @IntentionFamilyName + private final String myFamilyName; + + private NavigateToAction(NavigatablePsiElement navigatablePsiElement) { + super(navigatablePsiElement); + myFamilyName = + QuickFixBundle.message("show.duplicate.elements.navigate.family", JavaElementKind.fromElement(navigatablePsiElement).object()); + } + + @Override + public String getFamilyName() { + return myFamilyName; + } + + @Override + protected Presentation getPresentation(ActionContext context, NavigatablePsiElement element) { + int lineNumber = element.getContainingFile().getFileDocument().getLineNumber(element.getTextOffset()); + var title = QuickFixBundle.message("show.duplicate.elements.navigate.text", (lineNumber + 1)); + return Presentation.of(title).withHighlighting(element.getTextRange()); + } + + @Override + protected IntentionPreviewInfo generatePreview(ActionContext context, NavigatablePsiElement element) { + return IntentionPreviewInfo.snippet(element); + } + + + @Override + protected ModCommand perform(ActionContext context, NavigatablePsiElement element) { + return NavigateToDuplicateElementFix.createSelectCommand(element); + } + } +} diff --git a/java/java-impl/src/com/intellij/codeInsight/intention/impl/config/QuickFixFactoryImpl.java b/java/java-impl/src/com/intellij/codeInsight/intention/impl/config/QuickFixFactoryImpl.java index 922acfc8ea6c..028a3fa13a25 100644 --- a/java/java-impl/src/com/intellij/codeInsight/intention/impl/config/QuickFixFactoryImpl.java +++ b/java/java-impl/src/com/intellij/codeInsight/intention/impl/config/QuickFixFactoryImpl.java @@ -246,6 +246,11 @@ public final class QuickFixFactoryImpl extends QuickFixFactory { return new NavigateToDuplicateElementFix(element).asIntention(); } + @Override + public @NotNull IntentionAction createShowDuplicateElementsFix(@NotNull List<@NotNull ? extends NavigatablePsiElement> elements) { + return new ShowDuplicateElementsFix(elements).asIntention(); + } + @Override public @NotNull IntentionAction createConvertToStringLiteralAction() { return new ConvertToStringLiteralAction().asIntention(); diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/afterNavigatesFromFirstToSecond.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/afterNavigatesFromFirstToSecond.java similarity index 72% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/afterNavigatesFromFirstToSecond.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/afterNavigatesFromFirstToSecond.java index c5a87f637460..1a8a6bf03550 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/afterNavigatesFromFirstToSecond.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/afterNavigatesFromFirstToSecond.java @@ -1,4 +1,4 @@ -// "Navigate to duplicate method" "true" +// "Show 'foo()' duplicates|->Line #5" "true" public class MyClass { public void foo() {} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/afterNavigatesFromFirstToSecondGenericMethod.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/afterNavigatesFromFirstToSecondGenericMethod.java similarity index 76% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/afterNavigatesFromFirstToSecondGenericMethod.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/afterNavigatesFromFirstToSecondGenericMethod.java index e4ff84c9774d..96cb5e2a0a15 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/afterNavigatesFromFirstToSecondGenericMethod.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/afterNavigatesFromFirstToSecondGenericMethod.java @@ -1,4 +1,4 @@ -// "Navigate to duplicate method" "true" +// "Show 'foo()' duplicates|->Line #5" "true" public class MyClass { public void foo(T bar) {} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/afterNavigatesFromThirdToFirst.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/afterNavigatesFromThirdToFirst.java similarity index 72% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/afterNavigatesFromThirdToFirst.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/afterNavigatesFromThirdToFirst.java index dcd4b88e103d..c45b030d1b39 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/afterNavigatesFromThirdToFirst.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/afterNavigatesFromThirdToFirst.java @@ -1,4 +1,4 @@ -// "Navigate to duplicate method" "true" +// "Show 'foo()' duplicates|->Line #4" "true" public class MyClass { public void foo() {} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/afterNavigatesToEnumValueOfWhenDeclaredTwice.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/afterNavigatesToEnumValueOfWhenDeclaredTwice.java similarity index 77% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/afterNavigatesToEnumValueOfWhenDeclaredTwice.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/afterNavigatesToEnumValueOfWhenDeclaredTwice.java index b2b9f4c0211e..5532d19755a5 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/afterNavigatesToEnumValueOfWhenDeclaredTwice.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/afterNavigatesToEnumValueOfWhenDeclaredTwice.java @@ -1,4 +1,4 @@ -// "Navigate to duplicate method" "true" +// "Show 'valueOf()' duplicates|->Line #8" "true" enum MyEnum { FIRST; diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/afterNavigatesToEnumValuesWhenDeclaredTwice.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/afterNavigatesToEnumValuesWhenDeclaredTwice.java similarity index 75% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/afterNavigatesToEnumValuesWhenDeclaredTwice.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/afterNavigatesToEnumValuesWhenDeclaredTwice.java index b5d06b3784d5..9fc55a6aff70 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/afterNavigatesToEnumValuesWhenDeclaredTwice.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/afterNavigatesToEnumValuesWhenDeclaredTwice.java @@ -1,4 +1,4 @@ -// "Navigate to duplicate method" "true" +// "Show 'values()' duplicates|->Line #8" "true" enum MyEnum { FIRST; diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeDoesntNavigateToEnumValueOf.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeDoesntNavigateToEnumValueOf.java similarity index 67% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeDoesntNavigateToEnumValueOf.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeDoesntNavigateToEnumValueOf.java index 76b0074260f6..59f0ac242019 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeDoesntNavigateToEnumValueOf.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeDoesntNavigateToEnumValueOf.java @@ -1,4 +1,4 @@ -// "Navigate to duplicate method" "false" +// "Show 'foo()' duplicates" "false" enum MyEnum { FIRST; diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeDoesntNavigateToEnumValues.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeDoesntNavigateToEnumValues.java similarity index 66% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeDoesntNavigateToEnumValues.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeDoesntNavigateToEnumValues.java index 0db2a6210b97..9554cc07292d 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeDoesntNavigateToEnumValues.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeDoesntNavigateToEnumValues.java @@ -1,4 +1,4 @@ -// "Navigate to duplicate method" "false" +// "Show 'values()' duplicates" "false" enum MyEnum { FIRST; diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeNavigatesFromFirstToSecond.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeNavigatesFromFirstToSecond.java similarity index 68% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeNavigatesFromFirstToSecond.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeNavigatesFromFirstToSecond.java index d13831c2f596..2f39441ab7e5 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeNavigatesFromFirstToSecond.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeNavigatesFromFirstToSecond.java @@ -1,4 +1,4 @@ -// "Navigate to duplicate method" "true" +// "Show 'foo()' duplicates|->Line #5" "true" public class MyClass { public void foo() {} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeNavigatesFromFirstToSecondGenericMethod.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeNavigatesFromFirstToSecondGenericMethod.java similarity index 73% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeNavigatesFromFirstToSecondGenericMethod.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeNavigatesFromFirstToSecondGenericMethod.java index bfd5c39aeda9..5bdf7e82a41d 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeNavigatesFromFirstToSecondGenericMethod.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeNavigatesFromFirstToSecondGenericMethod.java @@ -1,4 +1,4 @@ -// "Navigate to duplicate method" "true" +// "Show 'foo()' duplicates|->Line #5" "true" public class MyClass { public void foo(T bar) {} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeNavigatesFromThirdToFirst.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeNavigatesFromThirdToFirst.java similarity index 68% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeNavigatesFromThirdToFirst.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeNavigatesFromThirdToFirst.java index 88d43abf483b..72358a74456f 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeNavigatesFromThirdToFirst.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeNavigatesFromThirdToFirst.java @@ -1,4 +1,4 @@ -// "Navigate to duplicate method" "true" +// "Show 'foo()' duplicates|->Line #4" "true" public class MyClass { public void foo() {} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeNavigatesToEnumValueOfWhenDeclaredTwice.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeNavigatesToEnumValueOfWhenDeclaredTwice.java similarity index 74% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeNavigatesToEnumValueOfWhenDeclaredTwice.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeNavigatesToEnumValueOfWhenDeclaredTwice.java index a3751a12b8e6..bf07d4719a85 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeNavigatesToEnumValueOfWhenDeclaredTwice.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeNavigatesToEnumValueOfWhenDeclaredTwice.java @@ -1,4 +1,4 @@ -// "Navigate to duplicate method" "true" +// "Show 'valueOf()' duplicates|->Line #8" "true" enum MyEnum { FIRST; diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeNavigatesToEnumValuesWhenDeclaredTwice.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeNavigatesToEnumValuesWhenDeclaredTwice.java similarity index 72% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeNavigatesToEnumValuesWhenDeclaredTwice.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeNavigatesToEnumValuesWhenDeclaredTwice.java index 92faa00fba87..30cefa6c434e 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix/beforeNavigatesToEnumValuesWhenDeclaredTwice.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix/beforeNavigatesToEnumValuesWhenDeclaredTwice.java @@ -1,4 +1,4 @@ -// "Navigate to duplicate method" "true" +// "Show 'values()' duplicates|->Line #8" "true" enum MyEnum { FIRST; diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/quickfix/NavigateToDuplicateMethodFixTest.java b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/quickfix/NavigateToDuplicateMethodFixTest.java deleted file mode 100644 index 3aeeb98d42c1..000000000000 --- a/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/quickfix/NavigateToDuplicateMethodFixTest.java +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.intellij.codeInsight.daemon.impl.quickfix; - -import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase; - -public class NavigateToDuplicateMethodFixTest extends LightQuickFixParameterizedTestCase { - @Override - protected String getBasePath() { - return "/codeInsight/daemonCodeAnalyzer/quickFix/navigateToDuplicateMethodFix"; - } -} diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/quickfix/ShowDuplicateMethodsFixTest.java b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/quickfix/ShowDuplicateMethodsFixTest.java new file mode 100644 index 000000000000..151d830df801 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/quickfix/ShowDuplicateMethodsFixTest.java @@ -0,0 +1,28 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInsight.daemon.impl.quickfix; + +import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase; +import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.ui.ChooserInterceptor; +import com.intellij.ui.UiInterceptors; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ShowDuplicateMethodsFixTest extends LightQuickFixParameterizedTestCase { + @Override + protected String getBasePath() { + return "/codeInsight/daemonCodeAnalyzer/quickFix/showDuplicateMethodsFix"; + } + + @Override + protected boolean shouldBeAvailableAfterExecution() { + return true; + } +} diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewDiffResultTest.kt b/java/java-tests/testSrc/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewDiffResultTest.kt index 15a70f75417d..f7bcfabcddd3 100644 --- a/java/java-tests/testSrc/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewDiffResultTest.kt +++ b/java/java-tests/testSrc/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewDiffResultTest.kt @@ -294,7 +294,22 @@ class IntentionPreviewDiffResultTest : LightPlatformCodeInsightFixture4TestCase( ).formatResult() assertEquals("", result) } - + + @Test + fun testSnippet() { + val text = """ + public void method() { + int a = 1; + } + """.trimIndent() + val result = IntentionPreviewDiffResult.fromSnippet(IntentionPreviewInfo.Snippet(JavaFileType.INSTANCE, text, 5)).formatResult() + assertEquals(""" + 5 : public void method() { + 6 : int a = 1; + 7 : } + """.trimIndent(), result) + } + /** * Returns a textual representation of diffs from created and modified text, how they will look * in the intention preview window. Each diff chunk is separated via a horizontal dashed line. diff --git a/platform/analysis-api/api-dump-experimental.txt b/platform/analysis-api/api-dump-experimental.txt index 2425327eaa18..140e393b1990 100644 --- a/platform/analysis-api/api-dump-experimental.txt +++ b/platform/analysis-api/api-dump-experimental.txt @@ -82,6 +82,17 @@ a:com.intellij.codeInsight.completion.CompletionService - highlightMessage():java.lang.String - highlightReference(com.intellij.lang.annotation.AnnotationBuilder):com.intellij.lang.annotation.AnnotationBuilder - highlightSeverity():com.intellij.lang.annotation.HighlightSeverity +com.intellij.codeInsight.intention.preview.IntentionPreviewInfo +- *s:snippet(com.intellij.psi.NavigatablePsiElement):com.intellij.codeInsight.intention.preview.IntentionPreviewInfo$Snippet +*f:com.intellij.codeInsight.intention.preview.IntentionPreviewInfo$Snippet +- java.lang.Record +- com.intellij.codeInsight.intention.preview.IntentionPreviewInfo +- (com.intellij.openapi.fileTypes.FileType,java.lang.String,I):V +- f:equals(java.lang.Object):Z +- fileType():com.intellij.openapi.fileTypes.FileType +- f:hashCode():I +- startLine():I +- text():java.lang.String f:com.intellij.codeInsight.intention.preview.IntentionPreviewUtils - *s:getModCommandPreview(com.intellij.modcommand.ModCommand,com.intellij.modcommand.ActionContext):com.intellij.codeInsight.intention.preview.IntentionPreviewInfo com.intellij.codeInsight.lookup.LookupEx diff --git a/platform/analysis-api/api-dump.txt b/platform/analysis-api/api-dump.txt index 08c28107a5a9..12a091fde278 100644 --- a/platform/analysis-api/api-dump.txt +++ b/platform/analysis-api/api-dump.txt @@ -128,6 +128,7 @@ com.intellij.codeInsight.intention.preview.IntentionPreviewInfo - s:moveToDirectory(com.intellij.openapi.vfs.VirtualFile,com.intellij.openapi.vfs.VirtualFile):com.intellij.codeInsight.intention.preview.IntentionPreviewInfo - s:navigate(com.intellij.psi.NavigatablePsiElement):com.intellij.codeInsight.intention.preview.IntentionPreviewInfo - s:navigate(com.intellij.psi.PsiFile,I):com.intellij.codeInsight.intention.preview.IntentionPreviewInfo$Html +- s:navigatePreviewHtmlChunk(com.intellij.psi.PsiFile,I):com.intellij.openapi.util.text.HtmlChunk - s:rename(com.intellij.openapi.vfs.VirtualFile,java.lang.String):com.intellij.codeInsight.intention.preview.IntentionPreviewInfo - s:rename(com.intellij.psi.PsiFile,java.lang.String):com.intellij.codeInsight.intention.preview.IntentionPreviewInfo com.intellij.codeInsight.lookup.Lookup diff --git a/platform/analysis-api/src/com/intellij/codeInsight/intention/preview/IntentionPreviewInfo.java b/platform/analysis-api/src/com/intellij/codeInsight/intention/preview/IntentionPreviewInfo.java index a28775d982a9..ae07015f6952 100644 --- a/platform/analysis-api/src/com/intellij/codeInsight/intention/preview/IntentionPreviewInfo.java +++ b/platform/analysis-api/src/com/intellij/codeInsight/intention/preview/IntentionPreviewInfo.java @@ -8,6 +8,7 @@ import com.intellij.openapi.editor.Editor; import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.NlsSafe; +import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.HtmlBuilder; import com.intellij.openapi.util.text.HtmlChunk; import com.intellij.openapi.vfs.VfsUtilCore; @@ -450,6 +451,10 @@ public interface IntentionPreviewInfo { * @return a presentation describing that the action will navigate to the specified target element */ static @NotNull Html navigate(@NotNull PsiFile file, int offset) { + return new Html(navigatePreviewHtmlChunk(file, offset).wrapWith("p")); + } + + static @NotNull HtmlChunk navigatePreviewHtmlChunk(@NotNull PsiFile file, int offset) { Icon icon = file.getIcon(0); Document document = file.getFileDocument(); HtmlBuilder builder = new HtmlBuilder(); @@ -461,7 +466,7 @@ public interface IntentionPreviewInfo { builder.append(AnalysisBundle.message("html.preview.navigate.line")) .append(String.valueOf(lineNumber+1)); } - return new Html(builder.wrapWith("p")); + return builder.toFragment(); } /** @@ -497,4 +502,35 @@ public interface IntentionPreviewInfo { .toFragment(); return new Html(content); } + + /** + * Preview of a code snippet. It will be rendered using the color scheme configured for specified {@code fileType} + * with line numbers. + * + * @param fileType file type of code snippet + * @param text snippet text + * @param startLine line number of the first line of the snippet + */ + @ApiStatus.Experimental + record Snippet(@NotNull FileType fileType, @NotNull String text, int startLine) implements IntentionPreviewInfo { + } + + /** + * Creates snippet preview containing text of {@code element}. + * + * @param element element which text will be used as a snippet + * @return snippet preview for given element + */ + @ApiStatus.Experimental + static @NotNull Snippet snippet(@NotNull NavigatablePsiElement element) { + var textRange = element.getTextRange(); + var document = element.getContainingFile().getFileDocument(); + int startOffset = textRange.getStartOffset(); + int startLine = document.getLineNumber(startOffset); + int startOffsetOfFirstLine = document.getLineStartOffset(startLine); + int endOffset = textRange.getEndOffset(); + var fileType = element.getContainingFile().getFileType(); + var text = document.getText(TextRange.create(startOffsetOfFirstLine, endOffset)).stripIndent(); + return new Snippet(fileType, text, startLine); + } } diff --git a/platform/lang-impl/api-dump-unreviewed.txt b/platform/lang-impl/api-dump-unreviewed.txt index f22f9c537739..65d8ab9792c4 100644 --- a/platform/lang-impl/api-dump-unreviewed.txt +++ b/platform/lang-impl/api-dump-unreviewed.txt @@ -4175,6 +4175,7 @@ f:com.intellij.codeInsight.intention.impl.preview.IntentionPreviewDiffResult - equals(java.lang.Object):Z - sf:fromCustomDiff(com.intellij.codeInsight.intention.preview.IntentionPreviewInfo$CustomDiff):com.intellij.codeInsight.intention.impl.preview.IntentionPreviewDiffResult - sf:fromMultiDiff(com.intellij.codeInsight.intention.preview.IntentionPreviewInfo$MultiFileDiff):com.intellij.codeInsight.intention.impl.preview.IntentionPreviewDiffResult +- sf:fromSnippet(com.intellij.codeInsight.intention.preview.IntentionPreviewInfo$Snippet):com.intellij.codeInsight.intention.impl.preview.IntentionPreviewDiffResult - f:getDiffs():java.util.List - f:getNewText():java.lang.String - hashCode():I @@ -4184,6 +4185,7 @@ f:com.intellij.codeInsight.intention.impl.preview.IntentionPreviewDiffResult$Com - bs:create$default(com.intellij.codeInsight.intention.impl.preview.IntentionPreviewDiffResult$Companion,com.intellij.openapi.fileTypes.FileType,java.lang.String,java.lang.String,com.intellij.diff.comparison.ComparisonPolicy,Z,java.lang.String,I,java.lang.Object):com.intellij.codeInsight.intention.impl.preview.IntentionPreviewDiffResult - f:fromCustomDiff(com.intellij.codeInsight.intention.preview.IntentionPreviewInfo$CustomDiff):com.intellij.codeInsight.intention.impl.preview.IntentionPreviewDiffResult - f:fromMultiDiff(com.intellij.codeInsight.intention.preview.IntentionPreviewInfo$MultiFileDiff):com.intellij.codeInsight.intention.impl.preview.IntentionPreviewDiffResult +- f:fromSnippet(com.intellij.codeInsight.intention.preview.IntentionPreviewInfo$Snippet):com.intellij.codeInsight.intention.impl.preview.IntentionPreviewDiffResult f:com.intellij.codeInsight.intention.impl.preview.IntentionPreviewDiffResult$DiffInfo - (com.intellij.openapi.fileTypes.FileType,java.lang.String,I,I,java.util.List):V - f:component1():com.intellij.openapi.fileTypes.FileType diff --git a/platform/lang-impl/src/com/intellij/codeInsight/completion/command/CommandCompletionDocumentationProvider.kt b/platform/lang-impl/src/com/intellij/codeInsight/completion/command/CommandCompletionDocumentationProvider.kt index 82d8a4bc0aa5..6daf46173598 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/completion/command/CommandCompletionDocumentationProvider.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/completion/command/CommandCompletionDocumentationProvider.kt @@ -277,6 +277,7 @@ private class CommandCompletionDocumentationTarget( private fun postprocess(info: IntentionPreviewInfo) = when (info) { is IntentionPreviewInfo.CustomDiff -> IntentionPreviewDiffResult.fromCustomDiff(info) is IntentionPreviewInfo.MultiFileDiff -> IntentionPreviewDiffResult.fromMultiDiff(info) + is IntentionPreviewInfo.Snippet -> IntentionPreviewDiffResult.fromSnippet(info) else -> info } } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewDiffResult.kt b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewDiffResult.kt index a28904b0cd5e..6274d2e2b68c 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewDiffResult.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewDiffResult.kt @@ -21,6 +21,15 @@ data class IntentionPreviewDiffResult(val diffs: List, @TestOnly val n data class Fragment(val type: HighlightingType, val start: Int, val end: Int) + /** + * Preview texts with specified fragment highlighting. + * + * @param fileType type of file from which fileText is taken + * @param fileText preview text + * @param startLine line in the file where fileText starts + * @param length fileText's line count + * @param fragments list fragments to be highlighted in preview + */ data class DiffInfo( val fileType: FileType, val fileText: String, @@ -178,5 +187,11 @@ data class IntentionPreviewDiffResult(val diffs: List, @TestOnly val n val text = diffs.joinToString("\n----------\n") { diff -> diff.newText } return IntentionPreviewDiffResult(diffInfos, text) } + + @JvmStatic + fun fromSnippet(info: IntentionPreviewInfo.Snippet): IntentionPreviewDiffResult { + val diffInfo = DiffInfo(info.fileType, info.text, info.startLine, info.text.lines().size, listOf()) + return IntentionPreviewDiffResult(listOf(diffInfo), info.text) + } } } \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewPopupUpdateProcessor.kt b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewPopupUpdateProcessor.kt index b5e58d4ece07..7357e298b7e6 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewPopupUpdateProcessor.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewPopupUpdateProcessor.kt @@ -305,6 +305,7 @@ class IntentionPreviewPopupUpdateProcessor internal constructor( private fun postprocess(info: IntentionPreviewInfo) = when (info) { is IntentionPreviewInfo.CustomDiff -> IntentionPreviewDiffResult.fromCustomDiff(info) is IntentionPreviewInfo.MultiFileDiff -> IntentionPreviewDiffResult.fromMultiDiff(info) + is IntentionPreviewInfo.Snippet -> IntentionPreviewDiffResult.fromSnippet(info) else -> info }