[java-inspections] MoveMemberIntoClassFix: better preview

GitOrigin-RevId: fab65ff4e1dd7038cc1110ac4b3b8d2f96fce59b
This commit is contained in:
Tagir Valeev
2022-07-12 11:43:41 +00:00
committed by intellij-monorepo-bot
parent c9a1f639a9
commit ef2fc85c6d
3 changed files with 106 additions and 32 deletions
@@ -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<TextRange, PsiMember> 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<PsiClass> 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<TextRange, PsiMember> 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);
}
}
@@ -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 <caret>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("<p><icon src=\"file\"/>&nbsp;Test.java &rarr; <icon src=\"file\"/>&nbsp;Best.java</p>",
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 <caret>method() {}");
IntentionPreviewInfo.Html info = getPreviewHtml("Move member into class");
assertEquals("<p><icon src=\"source\"/>&nbsp;method &rarr; <icon src=\"target\"/>&nbsp;Test</p>",
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 {
@@ -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("&rarr;")).append(" ")
.append(targetIconChunk)
.append(Objects.requireNonNull(target.getName()))
.toFragment();
Map<String, Icon> iconMap = new HashMap<>();
iconMap.put("source", sourceIcon);
iconMap.put("target", targetIcon);
return new Html(fragment.wrapWith("p"), iconMap);
}
}