diff --git a/java/java-analysis-impl/src/messages/QuickFixBundle.properties b/java/java-analysis-impl/src/messages/QuickFixBundle.properties index b5bffc3a6486..06fe26900035 100644 --- a/java/java-analysis-impl/src/messages/QuickFixBundle.properties +++ b/java/java-analysis-impl/src/messages/QuickFixBundle.properties @@ -101,6 +101,8 @@ create.member.from.usage.family=Create member from usage create.method.from.usage.family=Create method from usage create.method.from.usage.text=Create method ''{0}'' create.method.from.usage.full.text=Create method ''{0}'' in ''{1}'' +create.type.parameter.from.usage.family=Create type parameter from usage +create.type.parameter.from.usage.text=Create type parameter ''{0}'' create.abstract.method.from.usage.text=Create abstract method ''{0}'' create.abstract.method.from.usage.full.text=Create abstract method ''{0}'' in ''{1}'' create.parameter.from.usage.family=Create parameter from Usage diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/CreateTypeParameterFromUsageFix.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/CreateTypeParameterFromUsageFix.java new file mode 100644 index 000000000000..ed63ce179e92 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/CreateTypeParameterFromUsageFix.java @@ -0,0 +1,235 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.codeInsight.daemon.impl.quickfix; + +import com.intellij.codeInsight.daemon.QuickFixBundle; +import com.intellij.codeInsight.intention.impl.BaseIntentionAction; +import com.intellij.ide.highlighter.JavaFileType; +import com.intellij.openapi.application.Application; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.LogicalPosition; +import com.intellij.openapi.editor.ScrollType; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.popup.JBPopupAdapter; +import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.openapi.ui.popup.LightweightWindowEvent; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.*; +import com.intellij.psi.util.PsiUtil; +import com.intellij.ui.components.JBList; +import com.intellij.util.IncorrectOperationException; +import com.intellij.util.SmartList; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import static com.intellij.util.ObjectUtils.tryCast; + +public class CreateTypeParameterFromUsageFix extends BaseIntentionAction { + private final SmartPsiElementPointer myRef; + + public CreateTypeParameterFromUsageFix(PsiJavaCodeReferenceElement refElement) { + myRef = SmartPointerManager.getInstance(refElement.getProject()).createSmartPsiElementPointer(refElement); + } + + @Nullable + private PsiJavaCodeReferenceElement getElement() { + return myRef.getElement(); + } + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getFamilyName() { + return QuickFixBundle.message("create.type.parameter.from.usage.family"); + } + + @Override + public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { + PsiJavaCodeReferenceElement element = getElement(); + if (element == null) return false; + Context context = Context.from(element); + boolean available = context != null; + if (available) { + setText(QuickFixBundle.message("create.type.parameter.from.usage.text", context.typeName)); + } + return available; + } + + @Override + public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { + PsiJavaCodeReferenceElement element = getElement(); + if (element == null) return; + Context context = Context.from(element); + if (context == null) return; + List placesToAdd = context.myPlacesToAdd; + + Application application = ApplicationManager.getApplication(); + if (placesToAdd.size() == 1 || application.isUnitTestMode()) { + PsiElement first = placesToAdd.get(0); + createTypeParameter(first, context.typeName); + } + else { + List toShow = new ArrayList<>(); + for (PsiNameIdentifierOwner owner : placesToAdd) { + toShow.add(owner.getName()); + } + AtomicReference rangeHighlighter = new AtomicReference<>(); // to change in lambda + MarkupModel markupModel = editor.getMarkupModel(); + + JBList list = new JBList<>(toShow); + list.addListSelectionListener(e -> { + dropHighlight(rangeHighlighter); + int selectedIndex = list.getSelectedIndex(); + if (selectedIndex < 0) return; + PsiNameIdentifierOwner elementToHighlight = placesToAdd.get(selectedIndex); + TextRange range = elementToHighlight.getTextRange(); + final LogicalPosition logicalPosition = editor.offsetToLogicalPosition(range.getStartOffset()); + editor.getScrollingModel().scrollTo(logicalPosition, ScrollType.MAKE_VISIBLE); + TextAttributes attributes = + EditorColorsManager.getInstance().getGlobalScheme().getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES); + RangeHighlighter highlighter = markupModel.addRangeHighlighter(range.getStartOffset(), range.getEndOffset(), + HighlighterLayer.SELECTION - 1, attributes, + HighlighterTargetArea.EXACT_RANGE); + rangeHighlighter.set(highlighter); + }); + JBPopupFactory.getInstance() + .createListPopupBuilder(list) + .setTitle("Select place to add type parameter") + .setMovable(false) + .setResizable(false) + .setRequestFocus(true) + .setItemChoosenCallback(() -> { + int selectedIndex = list.getSelectedIndex(); + PsiNameIdentifierOwner owner = placesToAdd.get(selectedIndex); + createTypeParameter(owner, context.typeName); + }) + .addListener(new JBPopupAdapter() { + @Override + public void onClosed(LightweightWindowEvent event) { + dropHighlight(rangeHighlighter); + } + }) + .createPopup() + .showInBestPositionFor(editor); + + } + } + + private static void dropHighlight(AtomicReference rangeHighlighter) { + RangeHighlighter old = rangeHighlighter.get(); + if (old != null) { + old.dispose(); + } + } + + private static void createTypeParameter(@NotNull PsiElement methodOrClass, @NotNull String name) { + Project project = methodOrClass.getProject(); + WriteCommandAction.runWriteCommandAction(project, () -> { + PsiTypeParameterListOwner typeParameterListOwner = tryCast(methodOrClass, PsiTypeParameterListOwner.class); + if (typeParameterListOwner == null) { + throw new IllegalStateException("Only methods and classes allowed here, but was: " + methodOrClass.getClass()); + } + PsiTypeParameterList typeParameterList = typeParameterListOwner.getTypeParameterList(); + final String typeParameterListText; + if (typeParameterList == null) { + typeParameterListText = "<" + name + ">"; + } + else { + String existingTypeParameterText = typeParameterList.getText(); + if (typeParameterList.getTypeParameters().length == 0) { + typeParameterListText = "<" + name + ">"; + } + else { + String prefix = existingTypeParameterText.substring(0, existingTypeParameterText.length() - 1); + typeParameterListText = prefix + ", " + name + ">"; + } + } + PsiTypeParameterList newTypeParameterList = createTypeParameterList(typeParameterListText, project); + replaceOrAddTypeParameterList(methodOrClass, typeParameterList, newTypeParameterList); + }); + } + + private static void replaceOrAddTypeParameterList(@NotNull PsiElement methodOrClass, + @Nullable PsiTypeParameterList typeParameterList, + @NotNull PsiTypeParameterList newTypeParameterList) { + if (methodOrClass instanceof PsiMethod) { + PsiMethod method = (PsiMethod)methodOrClass; + if (typeParameterList == null) { + PsiTypeElement returnTypeElement = method.getReturnTypeElement(); + if (returnTypeElement == null) return; + method.addBefore(newTypeParameterList, returnTypeElement); + } + else { + typeParameterList.replace(newTypeParameterList); + } + } + else { + PsiClass aClass = (PsiClass)methodOrClass; + if (typeParameterList == null) { + PsiIdentifier nameIdentifier = aClass.getNameIdentifier(); + if (nameIdentifier == null) return; + aClass.addAfter(newTypeParameterList, nameIdentifier); + } + else { + typeParameterList.replace(newTypeParameterList); + } + } + } + + private static PsiTypeParameterList createTypeParameterList(@NotNull String text, Project project) { + PsiJavaFile javaFile = (PsiJavaFile)PsiFileFactory.getInstance(project) + .createFileFromText("_DUMMY_", JavaFileType.INSTANCE, + "class __DUMMY__ " + text + " {}"); + PsiClass[] classes = javaFile.getClasses(); + return classes[0].getTypeParameterList(); + } + + private static class Context { + @NotNull final List myPlacesToAdd; + @NotNull final String typeName; + + Context(@NotNull List add, @NotNull String name) { + myPlacesToAdd = add; + typeName = name; + } + + @Nullable + static Context from(@NotNull PsiJavaCodeReferenceElement element) { + if (!PsiUtil.isLanguageLevel5OrHigher(element)) return null; + if (element.isQualified()) return null; + List candidates = collectParentClassesAndMethodUntilStatic(element); + if (candidates.isEmpty()) return null; + String name = element.getReferenceName(); + if (name == null) return null; + candidates = candidates.stream().filter(owner -> owner.getName() != null).collect(Collectors.toList()); + return new Context(candidates, name); + } + } + + + static List collectParentClassesAndMethodUntilStatic(PsiElement element) { + element = element.getParent(); + List parents = new SmartList<>(); + while (element != null) { + if (element instanceof PsiField && ((PsiField)element).hasModifierProperty(PsiModifier.STATIC)) { + break; + } + if (element instanceof PsiMethod || element instanceof PsiClass) { + parents.add((PsiNameIdentifierOwner)element); + if (((PsiModifierListOwner)element).hasModifierProperty(PsiModifier.STATIC)) break; + } + element = element.getParent(); + } + return parents; + } +} diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/DefaultQuickFixProvider.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/DefaultQuickFixProvider.java index c5f6c495cb10..144bfd2019ea 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/DefaultQuickFixProvider.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/DefaultQuickFixProvider.java @@ -61,6 +61,7 @@ public class DefaultQuickFixProvider extends UnresolvedReferenceQuickFixProvider if (PsiUtil.isLanguageLevel5OrHigher(ref)) { registrar.register(new CreateClassFromUsageFix(ref, CreateClassKind.ENUM)); registrar.register(new CreateClassFromUsageFix(ref, CreateClassKind.ANNOTATION)); + registrar.register(new CreateTypeParameterFromUsageFix(ref)); } PsiElement parent = PsiTreeUtil.getParentOfType(ref, PsiNewExpression.class, PsiMethod.class); diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/afterClass.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/afterClass.java new file mode 100644 index 000000000000..4322704ddd4c --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/afterClass.java @@ -0,0 +1,5 @@ +// "Create type parameter 'T'" "true" +import java.util.concurrent.Callable; + +public class Test extends Callable { +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/afterClassEmptyTypeParamList.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/afterClassEmptyTypeParamList.java new file mode 100644 index 000000000000..4322704ddd4c --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/afterClassEmptyTypeParamList.java @@ -0,0 +1,5 @@ +// "Create type parameter 'T'" "true" +import java.util.concurrent.Callable; + +public class Test extends Callable { +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/afterClassExistingParameter.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/afterClassExistingParameter.java new file mode 100644 index 000000000000..b57dee2b10d8 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/afterClassExistingParameter.java @@ -0,0 +1,5 @@ +// "Create type parameter 'T'" "true" +import java.util.concurrent.Callable; + +public class Test extends Callable { +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/afterInstanceMethod.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/afterInstanceMethod.java new file mode 100644 index 000000000000..b53182527480 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/afterInstanceMethod.java @@ -0,0 +1,7 @@ +// "Create type parameter 'T'" "true" + +public class Test { + T foo() { + + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/beforeClass.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/beforeClass.java new file mode 100644 index 000000000000..ce95d40482a2 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/beforeClass.java @@ -0,0 +1,5 @@ +// "Create type parameter 'T'" "true" +import java.util.concurrent.Callable; + +public class Test extends Callable> { +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/beforeClassEmptyTypeParamList.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/beforeClassEmptyTypeParamList.java new file mode 100644 index 000000000000..ce62fb9304a7 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/beforeClassEmptyTypeParamList.java @@ -0,0 +1,5 @@ +// "Create type parameter 'T'" "true" +import java.util.concurrent.Callable; + +public class Test<> extends Callable> { +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/beforeClassExistingParameter.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/beforeClassExistingParameter.java new file mode 100644 index 000000000000..5db873a0a0c8 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/beforeClassExistingParameter.java @@ -0,0 +1,5 @@ +// "Create type parameter 'T'" "true" +import java.util.concurrent.Callable; + +public class Test extends Callable> { +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/beforeInstanceMethod.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/beforeInstanceMethod.java new file mode 100644 index 000000000000..be1a7ff778e2 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage/beforeInstanceMethod.java @@ -0,0 +1,7 @@ +// "Create type parameter 'T'" "true" + +public class Test { + T foo() { + + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/quickFix/CreateTypeParameterFromUsageTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/quickFix/CreateTypeParameterFromUsageTest.java new file mode 100644 index 000000000000..834c3d666a97 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/quickFix/CreateTypeParameterFromUsageTest.java @@ -0,0 +1,13 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.java.codeInsight.daemon.quickFix; + +import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase; + +public class CreateTypeParameterFromUsageTest extends LightQuickFixParameterizedTestCase { + public void test() { doAllTests(); } + + @Override + protected String getBasePath() { + return "/codeInsight/daemonCodeAnalyzer/quickFix/createTypeParameterFromUsage"; + } +}