diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/MoveMemberIntoClassFix.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/MoveMemberIntoClassFix.java index 95dcc9767a4e..f47684393de3 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/MoveMemberIntoClassFix.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/MoveMemberIntoClassFix.java @@ -2,16 +2,17 @@ package com.intellij.codeInsight.daemon.impl.quickfix; import com.intellij.codeInsight.daemon.impl.analysis.MemberModel; +import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo; import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement; import com.intellij.java.JavaBundle; import com.intellij.lang.jvm.JvmModifier; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.util.ObjectUtils; import com.intellij.util.containers.ContainerUtil; @@ -32,27 +33,15 @@ public class MoveMemberIntoClassFix extends LocalQuickFixAndIntentionActionOnPsi @Nullable Editor editor, @NotNull PsiElement startElement, @NotNull PsiElement endElement) { - if (editor == null) return; - PsiJavaFile javaFile = ObjectUtils.tryCast(file, PsiJavaFile.class); - if (javaFile == null) return; - PsiErrorElement errorElement = ObjectUtils.tryCast(startElement, PsiErrorElement.class); - if (errorElement == null) return; - String className = FileUtilRt.getNameWithoutExtension(file.getName()); - if (!StringUtil.isJavaIdentifier(className)) return; - MemberModel model = MemberModel.create(errorElement); - if (model == null) return; - PsiClass psiClass = ContainerUtil.find(javaFile.getClasses(), c -> className.equals(c.getName())); - TextRange memberRange = model.textRange(); - Document document = editor.getDocument(); - String memberText = document.getText(memberRange); - PsiElementFactory factory = JavaPsiFacade.getElementFactory(project); + Pair rangeAndMember = createMember(file); + if (rangeAndMember == null) return; + Document document = file.getViewProvider().getDocument(); + TextRange memberRange = rangeAndMember.getFirst(); + PsiMember member = rangeAndMember.getSecond(); + PsiClass psiClass = getPsiClass(file, member); + if (psiClass == null) return; PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project); - MemberModel.MemberType memberType = model.memberType(); - PsiMember member = memberType.create(factory, memberText, file); - if (psiClass == null) { - psiClass = (PsiClass)file.add(createClass(factory, className, member)); - documentManager.doPostponedOperationsAndUnblockDocument(document); - } + documentManager.doPostponedOperationsAndUnblockDocument(document); SmartPsiElementPointer classPtr = SmartPointerManager.createPointer(psiClass); document.deleteString(memberRange.getStartOffset(), memberRange.getEndOffset()); documentManager.commitDocument(document); @@ -63,6 +52,18 @@ public class MoveMemberIntoClassFix extends LocalQuickFixAndIntentionActionOnPsi psiClass.addBefore(member, rBrace); } + @Nullable + private static PsiClass getPsiClass(@NotNull PsiFile file, @NotNull PsiMember member) { + String className = FileUtilRt.getNameWithoutExtension(file.getName()); + if (!StringUtil.isJavaIdentifier(className)) return null; + PsiJavaFile javaFile = ObjectUtils.tryCast(file, PsiJavaFile.class); + if (javaFile == null) return null; + PsiClass psiClass = ContainerUtil.find(javaFile.getClasses(), c -> className.equals(c.getName())); + if (psiClass != null) return psiClass; + PsiElementFactory factory = JavaPsiFacade.getElementFactory(file.getProject()); + return (PsiClass)file.add(createClass(factory, className, member)); + } + @Override public @NotNull String getText() { return getFamilyName(); @@ -85,4 +86,25 @@ public class MoveMemberIntoClassFix extends LocalQuickFixAndIntentionActionOnPsi } return factory.createInterface(className); } + + private @Nullable Pair<@NotNull TextRange, @NotNull PsiMember> createMember(@NotNull PsiFile file) { + PsiErrorElement errorElement = ObjectUtils.tryCast(getStartElement(), PsiErrorElement.class); + if (errorElement == null) return null; + MemberModel model = MemberModel.create(errorElement); + if (model == null) return null; + TextRange memberRange = model.textRange(); + String memberText = memberRange.substring(file.getText()); + PsiElementFactory factory = JavaPsiFacade.getElementFactory(file.getProject()); + MemberModel.MemberType memberType = model.memberType(); + return Pair.create(memberRange, memberType.create(factory, memberText, file)); + } + + @Override + public @NotNull IntentionPreviewInfo generatePreview(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) { + Pair member = createMember(file); + if (member == null || !(member.getSecond() instanceof PsiNamedElement)) return IntentionPreviewInfo.EMPTY; + PsiClass psiClass = getPsiClass(file, member.getSecond()); + if (psiClass == null) return IntentionPreviewInfo.EMPTY; + return IntentionPreviewInfo.movePsi((PsiNamedElement)member.getSecond(), psiClass); + } } diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/IntentionPreviewTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/IntentionPreviewTest.java index 1053beb7c056..701bb9ec0d39 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/IntentionPreviewTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/IntentionPreviewTest.java @@ -7,9 +7,9 @@ import com.intellij.codeInsight.intention.impl.preview.IntentionPreviewPopupUpda import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo; import com.intellij.codeInsight.template.TemplateManager; import com.intellij.openapi.application.ReadAction; -import com.intellij.openapi.util.text.HtmlChunk; import com.intellij.util.concurrency.AppExecutorUtil; import org.intellij.lang.regexp.inspection.DuplicateCharacterInClassInspection; +import org.jetbrains.annotations.NotNull; import java.util.concurrent.ExecutionException; @@ -153,14 +153,19 @@ public class IntentionPreviewTest extends LightQuickFixTestCase { public void testRenameFile() { configureFromFileText("Test.java", "public class Best {}"); - IntentionAction action = findActionWithText("Rename File"); - assertNotNull(action); - IntentionPreviewInfo info = IntentionPreviewPopupUpdateProcessor.getPreviewInfo(getProject(), action, getFile(), getEditor()); - assertTrue(info instanceof IntentionPreviewInfo.Html); - HtmlChunk content = ((IntentionPreviewInfo.Html)info).content(); + IntentionPreviewInfo.Html info = getPreviewHtml("Rename File"); assertEquals("

 Test.java →  Best.java

", - content.toString()); - assertNotNull(((IntentionPreviewInfo.Html)info).icon("file")); + info.content().toString()); + assertNotNull(info.icon("file")); + } + + public void testMoveMemberIntoClass() { + configureFromFileText("Test.java", "public class Test {} void method() {}"); + IntentionPreviewInfo.Html info = getPreviewHtml("Move member into class"); + assertEquals("

 method →  Test

", + info.content().toString()); + assertNotNull(info.icon("source")); + assertNotNull(info.icon("target")); } @Override @@ -168,6 +173,15 @@ public class IntentionPreviewTest extends LightQuickFixTestCase { // we want to stay at host editor } + @NotNull + private IntentionPreviewInfo.Html getPreviewHtml(String actionName) { + IntentionAction action = findActionWithText(actionName); + assertNotNull(action); + IntentionPreviewInfo info = IntentionPreviewPopupUpdateProcessor.getPreviewInfo(getProject(), action, getFile(), getEditor()); + assertTrue(info instanceof IntentionPreviewInfo.Html); + return (IntentionPreviewInfo.Html)info; + } + private String getPreviewText(IntentionAction action) { // Run in background thread to catch accidental write-actions during preview generation try { 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 947cc7ce88b1..e47e06952f03 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 @@ -10,7 +10,10 @@ import com.intellij.openapi.util.text.HtmlBuilder; import com.intellij.openapi.util.text.HtmlChunk; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiNamedElement; +import com.intellij.ui.DeferredIcon; import com.intellij.util.IconUtil; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nls; @@ -18,7 +21,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; +import java.util.HashMap; import java.util.Map; +import java.util.Objects; /** * Possible result for IntentionPreview. @@ -178,9 +183,7 @@ public interface IntentionPreviewInfo { */ static @NotNull IntentionPreviewInfo rename(@NotNull PsiFile file, @NotNull @NlsSafe String newName) { Icon icon = file.getIcon(0); - HtmlChunk iconChunk = - icon == null ? HtmlChunk.empty() : - new HtmlBuilder().append(HtmlChunk.tag("icon").attr("src", "file")).nbsp().toFragment(); + HtmlChunk iconChunk = getIconChunk(icon, "file"); HtmlChunk fragment = new HtmlBuilder() .append(iconChunk) .append(file.getName()) @@ -191,6 +194,12 @@ public interface IntentionPreviewInfo { return new Html(fragment.wrapWith("p"), icon == null ? Map.of() : Map.of("file", icon)); } + @NotNull + private static HtmlChunk getIconChunk(@Nullable Icon icon, @NotNull String id) { + return icon == null ? HtmlChunk.empty() : + new HtmlBuilder().append(HtmlChunk.tag("icon").attr("src", id)).nbsp().toFragment(); + } + /** * @param file file to be moved * @param directory target directory @@ -214,4 +223,33 @@ public interface IntentionPreviewInfo { .toFragment(); return new Html(fragment.wrapWith("p"), Map.of("file", fileIcon, "dir", dirIcon)); } + + /** + * @param source PsiElement to move + * @param target target PsiElement + * @return a presentation describing moving of source element to the target + */ + static @NotNull IntentionPreviewInfo movePsi(@NotNull PsiNamedElement source, @NotNull PsiNamedElement target) { + Icon sourceIcon = source.getIcon(0); + if (sourceIcon instanceof DeferredIcon) { + sourceIcon = ((DeferredIcon)sourceIcon).evaluate(); + } + Icon targetIcon = target.getIcon(0); + if (targetIcon instanceof DeferredIcon) { + targetIcon = ((DeferredIcon)targetIcon).evaluate(); + } + HtmlChunk sourceIconChunk = getIconChunk(sourceIcon, "source"); + HtmlChunk targetIconChunk = getIconChunk(targetIcon, "target"); + HtmlChunk fragment = new HtmlBuilder() + .append(sourceIconChunk) + .append(Objects.requireNonNull(source.getName())) + .append(" ").append(HtmlChunk.htmlEntity("→")).append(" ") + .append(targetIconChunk) + .append(Objects.requireNonNull(target.getName())) + .toFragment(); + Map iconMap = new HashMap<>(); + iconMap.put("source", sourceIcon); + iconMap.put("target", targetIcon); + return new Html(fragment.wrapWith("p"), iconMap); + } }