mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[mod-command] First experimental draft
GitOrigin-RevId: 58558851eb884fb4a288d788f9009a8e4cb133b8
This commit is contained in:
committed by
intellij-monorepo-bot
parent
c438670358
commit
42d9b29a47
+7
-23
@@ -1,13 +1,12 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.codeInspection;
|
||||
|
||||
import com.intellij.codeInsight.ExpressionUtil;
|
||||
import com.intellij.codeInsight.PsiEquivalenceUtil;
|
||||
import com.intellij.codeInsight.daemon.impl.analysis.HighlightControlFlowUtil;
|
||||
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo;
|
||||
import com.intellij.codeInspection.util.InspectionMessage;
|
||||
import com.intellij.ide.util.PsiNavigationSupport;
|
||||
import com.intellij.java.JavaBundle;
|
||||
import com.intellij.modcommand.ModCommand;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
@@ -229,7 +228,7 @@ public class OverwrittenKeyInspection extends AbstractBaseJavaLocalInspectionToo
|
||||
}
|
||||
}
|
||||
|
||||
private static class NavigateToDuplicateFix implements LocalQuickFix {
|
||||
private static class NavigateToDuplicateFix extends ModCommandQuickFix {
|
||||
private final SmartPsiElementPointer<PsiExpression> myPointer;
|
||||
|
||||
NavigateToDuplicateFix(PsiExpression arg) {
|
||||
@@ -244,30 +243,15 @@ public class OverwrittenKeyInspection extends AbstractBaseJavaLocalInspectionToo
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean startInWriteAction() {
|
||||
return false;
|
||||
public @NotNull ModCommand perform(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
|
||||
PsiExpression element = myPointer.getElement();
|
||||
if (element == null) return ModCommands.nop();
|
||||
return ModCommands.select(element);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean availableInBatchMode() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
|
||||
PsiExpression element = myPointer.getElement();
|
||||
if (element == null) return;
|
||||
PsiFile file = element.getContainingFile();
|
||||
if (file == null) return;
|
||||
int offset = element.getTextRange().getStartOffset();
|
||||
PsiNavigationSupport.getInstance().createNavigatable(project, file.getVirtualFile(), offset).navigate(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull IntentionPreviewInfo generatePreview(@NotNull Project project, @NotNull ProblemDescriptor previewDescriptor) {
|
||||
NavigatablePsiElement element = tryCast(myPointer.getElement(), NavigatablePsiElement.class);
|
||||
if (element == null) return IntentionPreviewInfo.EMPTY;
|
||||
return IntentionPreviewInfo.navigate(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-17
@@ -4,7 +4,6 @@ package com.intellij.codeInspection;
|
||||
import com.intellij.codeInsight.ExpressionUtil;
|
||||
import com.intellij.codeInsight.Nullability;
|
||||
import com.intellij.codeInsight.NullableNotNullManager;
|
||||
import com.intellij.codeInsight.intention.FileModifier;
|
||||
import com.intellij.codeInsight.intention.impl.StreamRefactoringUtil;
|
||||
import com.intellij.codeInspection.dataFlow.DfaUtil;
|
||||
import com.intellij.codeInspection.dataFlow.NullabilityUtil;
|
||||
@@ -12,6 +11,7 @@ import com.intellij.codeInspection.redundantCast.RemoveRedundantCastUtil;
|
||||
import com.intellij.codeInspection.util.InspectionMessage;
|
||||
import com.intellij.codeInspection.util.IntentionName;
|
||||
import com.intellij.java.JavaBundle;
|
||||
import com.intellij.modcommand.ModCommand;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
@@ -260,7 +260,7 @@ public class SimplifyStreamApiCallChainsInspection extends AbstractBaseJavaLocal
|
||||
return new TextRange(startOffset, endOffset).shiftRight(-expression.getTextOffset());
|
||||
}
|
||||
|
||||
interface CallChainFix extends FileModifier {
|
||||
interface CallChainFix {
|
||||
@IntentionName String getName();
|
||||
void applyFix(@NotNull Project project, PsiElement element);
|
||||
}
|
||||
@@ -283,7 +283,7 @@ public class SimplifyStreamApiCallChainsInspection extends AbstractBaseJavaLocal
|
||||
PsiElement simplify(PsiMethodCallExpression element);
|
||||
}
|
||||
|
||||
private static class SimplifyCallChainFix implements LocalQuickFix {
|
||||
private static class SimplifyCallChainFix extends ModCommandQuickFix {
|
||||
private final CallChainFix myFix;
|
||||
|
||||
SimplifyCallChainFix(CallChainFix fix) {
|
||||
@@ -303,14 +303,8 @@ public class SimplifyStreamApiCallChainsInspection extends AbstractBaseJavaLocal
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
|
||||
myFix.applyFix(project, descriptor.getStartElement());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable FileModifier getFileModifierForPreview(@NotNull PsiFile target) {
|
||||
CallChainFix newFix = (CallChainFix)myFix.getFileModifierForPreview(target);
|
||||
return newFix == myFix ? this : new SimplifyCallChainFix(newFix);
|
||||
public @NotNull ModCommand perform(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
|
||||
return ModCommands.psiUpdate(descriptor.getStartElement(), e -> myFix.applyFix(project, e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1680,12 +1674,6 @@ public class SimplifyStreamApiCallChainsInspection extends AbstractBaseJavaLocal
|
||||
myValuePointer = SmartPointerManager.createPointer(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable FileModifier getFileModifierForPreview(@NotNull PsiFile target) {
|
||||
PsiExpression expression = myValuePointer.getElement();
|
||||
return expression == null ? null : new AnyMatchContainsFix(expression);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return CommonQuickFixBundle.message("fix.replace.with.x", "List.contains()");
|
||||
|
||||
@@ -4,7 +4,7 @@ import java.util.*;
|
||||
|
||||
class Test {
|
||||
public boolean testAnyMatch(Map<String, Integer> map) {
|
||||
map.keySet().stream().forEach(System.out::println);
|
||||
map.keySet().stream().filter(x -> !x.isEmpty()).forEach(System.out::println);
|
||||
map.values().stream().forEach(System.out::println);
|
||||
/*3*/
|
||||
/*4*/
|
||||
|
||||
@@ -4,7 +4,7 @@ import java.util.*;
|
||||
|
||||
class Test {
|
||||
public boolean testAnyMatch(Map<String, Integer> map) {
|
||||
map.entrySet().stream().m<caret>ap(Map.Entry::getKey).forEach(System.out::println);
|
||||
map.entrySet().stream().m<caret>ap(Map.Entry::getKey).filter(x -> !x.isEmpty()).forEach(System.out::println);
|
||||
map.entrySet().stream().map(Map.Entry::getValue).forEach(System.out::println);
|
||||
map/*1*/.entrySet()/*2*/.stream()./*3*/map(e -> /*4*/e.getKey())/*5*/.forEach(System.out::println);
|
||||
map.entrySet().stream().map(e -> {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.modcommand;
|
||||
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.vfs.ReadonlyStatusHandler;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A transparent command, which modifies the project/workspace state (writes file, changes setting, moves editor caret, etc.),
|
||||
* or produces a user interaction (displays question, launches browser, etc.).
|
||||
* <p>
|
||||
* All inheritors are records, so the whole state is declarative and readable.
|
||||
*/
|
||||
public sealed interface ModCommand permits ModCompositeCommand, ModLegacyBridge, ModNavigate, ModNothing, ModUpdatePsiFile {
|
||||
/**
|
||||
* Executes the command
|
||||
*
|
||||
* @param project current project
|
||||
* @return execution status
|
||||
*/
|
||||
@RequiresEdt
|
||||
@NotNull ModStatus execute(@NotNull Project project);
|
||||
|
||||
/**
|
||||
* @return true if the command does nothing
|
||||
*/
|
||||
default boolean isEmpty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs preparatory step, if necessary. In particular unlocks necessary files for writing
|
||||
*
|
||||
* @return status of execution
|
||||
*/
|
||||
@RequiresEdt
|
||||
default @NotNull ModStatus prepare() {
|
||||
Set<PsiFile> files = modifiedFiles();
|
||||
if (files.isEmpty()) return ModStatus.SUCCESS;
|
||||
Project project = ContainerUtil.getFirstItem(files).getProject();
|
||||
VirtualFile[] vFiles = ContainerUtil.map2Array(files, VirtualFile.class, PsiFile::getVirtualFile);
|
||||
return ReadonlyStatusHandler.ensureFilesWritable(project, vFiles) ? ModStatus.SUCCESS : ModStatus.CANCEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return set of files that are potentially modified by this command
|
||||
*/
|
||||
default @NotNull Set<@NotNull PsiFile> modifiedFiles() {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to implement {@link #andThen(ModCommand)}. Should not be called directly.
|
||||
*
|
||||
* @param next command to be executed right after current
|
||||
* @return merged command that executes both this and next actions; null if merge is not possible.
|
||||
* Here, {@link ModCompositeCommand} is not returned
|
||||
* @see #andThen(ModCommand)
|
||||
*/
|
||||
default @Nullable ModCommand tryMerge(@NotNull ModCommand next) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param next command to be executed right after current
|
||||
* @return the composite command that executes both current and the next command
|
||||
*/
|
||||
default @NotNull ModCommand andThen(@NotNull ModCommand next) {
|
||||
if (isEmpty()) return next;
|
||||
if (next.isEmpty()) return this;
|
||||
ModCommand merged = tryMerge(next);
|
||||
if (merged != null) {
|
||||
return merged;
|
||||
}
|
||||
List<ModCommand> commands = new ArrayList<>(unpack());
|
||||
ModCommand last = Objects.requireNonNull(ContainerUtil.getLastItem(commands));
|
||||
List<ModCommand> nextCommands = next.unpack();
|
||||
for (int i = 0; i < nextCommands.size(); i++) {
|
||||
ModCommand command = nextCommands.get(i);
|
||||
merged = last.tryMerge(command);
|
||||
if (merged != null) {
|
||||
last = merged;
|
||||
} else {
|
||||
commands.set(commands.size() - 1, last);
|
||||
commands.addAll(nextCommands.subList(i, nextCommands.size()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return commands.size() == 1 ? commands.get(0) : new ModCompositeCommand(commands);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list of individual commands this command consists of
|
||||
*/
|
||||
default @NotNull List<@NotNull ModCommand> unpack() {
|
||||
return List.of(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.modcommand;
|
||||
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A composite command that contains leaf commands inside
|
||||
*
|
||||
* @param commands list of commands; must not contain composite commands
|
||||
*/
|
||||
public record ModCompositeCommand(@NotNull List<@NotNull ModCommand> commands) implements ModCommand {
|
||||
public ModCompositeCommand {
|
||||
commands = List.copyOf(commands);
|
||||
for (ModCommand command : commands) {
|
||||
if (command instanceof ModCompositeCommand) {
|
||||
throw new IllegalArgumentException("Nested composite command");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ModStatus execute(@NotNull Project project) {
|
||||
for (ModCommand command : commands) {
|
||||
ModStatus status = command.execute(project);
|
||||
if (status != ModStatus.SUCCESS) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
return ModStatus.SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return ContainerUtil.all(commands, ModCommand::isEmpty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Set<@NotNull PsiFile> modifiedFiles() {
|
||||
return commands.stream().flatMap(c -> c.modifiedFiles().stream()).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<@NotNull ModCommand> unpack() {
|
||||
return commands;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.modcommand;
|
||||
|
||||
import com.intellij.openapi.project.Project;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Opaque command that just executes a supplied runnable. Used to support legacy API,
|
||||
* should not be used in client code
|
||||
* @param runnable runnable to execute
|
||||
*/
|
||||
public record ModLegacyBridge(@NotNull Runnable runnable) implements ModCommand {
|
||||
@Override
|
||||
public @NotNull ModStatus execute(@NotNull Project project) {
|
||||
runnable.run();
|
||||
return ModStatus.SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.modcommand;
|
||||
|
||||
import com.intellij.openapi.editor.Editor;
|
||||
import com.intellij.openapi.fileEditor.FileEditor;
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager;
|
||||
import com.intellij.openapi.fileEditor.TextEditor;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A command that updates the selection and/or caret position in the editor
|
||||
*
|
||||
* @param file file that should be opened in the editor. The command aborts if the selected editor contains another file
|
||||
* @param selectionStart selection start; -1 if selection should not be changed
|
||||
* @param selectionEnd selection end; -1 if selection should not be changed
|
||||
* @param caret caret position; -1 if caret position should not be changed
|
||||
*/
|
||||
public record ModNavigate(@NotNull VirtualFile file, int selectionStart, int selectionEnd, int caret) implements ModCommand {
|
||||
@Override
|
||||
public @NotNull ModStatus execute(@NotNull Project project) {
|
||||
FileEditor fileEditor = FileEditorManager.getInstance(project).getSelectedEditor(file);
|
||||
if (fileEditor instanceof TextEditor textEditor) {
|
||||
Editor editor = textEditor.getEditor();
|
||||
if (selectionStart != -1 && selectionEnd != -1) {
|
||||
editor.getSelectionModel().setSelection(selectionStart, selectionEnd);
|
||||
}
|
||||
if (caret != -1) {
|
||||
editor.getCaretModel().moveToOffset(caret);
|
||||
}
|
||||
return ModStatus.SUCCESS;
|
||||
}
|
||||
return ModStatus.ABORT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.modcommand;
|
||||
|
||||
import com.intellij.openapi.project.Project;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A command that does nothing
|
||||
*/
|
||||
public record ModNothing() implements ModCommand {
|
||||
public static final ModNothing NOTHING = new ModNothing();
|
||||
|
||||
@Override
|
||||
public @NotNull ModStatus execute(@NotNull Project project) {
|
||||
return ModStatus.SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.modcommand;
|
||||
|
||||
/**
|
||||
* Status of the execution of {@link ModCommand}
|
||||
*/
|
||||
public enum ModStatus {
|
||||
/**
|
||||
* Operation completed successfully
|
||||
*/
|
||||
SUCCESS,
|
||||
/**
|
||||
* Operation aborted due to intermittent state change, no changes are introduced,
|
||||
* recreating the command and performing the operation again may be helpful
|
||||
*/
|
||||
ABORT,
|
||||
/**
|
||||
* Operation cancelled by user via UI
|
||||
*/
|
||||
CANCEL
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.modcommand;
|
||||
|
||||
import com.intellij.codeInsight.intention.preview.IntentionPreviewUtils;
|
||||
import com.intellij.openapi.editor.Document;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.psi.PsiDocumentManager;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A command that updates the content of a given {@link PsiFile}
|
||||
*
|
||||
* @param file file to update
|
||||
* @param oldText old text (expected). The command aborts if the old text doesn't match
|
||||
* @param newText new text
|
||||
*/
|
||||
public record ModUpdatePsiFile(@NotNull PsiFile file, @NotNull String oldText, @NotNull String newText) implements ModCommand {
|
||||
@Override
|
||||
public @NotNull ModStatus execute(@NotNull Project project) {
|
||||
return IntentionPreviewUtils.writeAndCompute(() -> {
|
||||
if (!file.textMatches(oldText)) return ModStatus.ABORT;
|
||||
Document document = file.getViewProvider().getDocument();
|
||||
document.replaceString(0, document.getTextLength(), newText);
|
||||
PsiDocumentManager.getInstance(project).commitDocument(document);
|
||||
return ModStatus.SUCCESS;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return oldText.equals(newText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Set<@NotNull PsiFile> modifiedFiles() {
|
||||
return Set.of(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ModCommand tryMerge(@NotNull ModCommand next) {
|
||||
if (next instanceof ModUpdatePsiFile update && file.isEquivalentTo(update.file) &&
|
||||
newText.equals(update.oldText)) {
|
||||
return new ModUpdatePsiFile(file, oldText, update.newText);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@ApiStatus.Experimental
|
||||
package com.intellij.modcommand;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.codeInspection;
|
||||
|
||||
import com.intellij.codeInsight.intention.FileModifier;
|
||||
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo;
|
||||
import com.intellij.modcommand.*;
|
||||
import com.intellij.model.SideEffectGuard;
|
||||
import com.intellij.openapi.application.ReadAction;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import com.intellij.psi.PsiManager;
|
||||
import com.intellij.util.concurrency.AppExecutorUtil;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* A novel experimental kind of quick-fix that creates a command instead of performing actual modification.
|
||||
* Default preview for this fix is based on the command returned from {@link #perform(Project, ProblemDescriptor)}
|
||||
* @see ModCommand
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
public abstract class ModCommandQuickFix implements LocalQuickFix {
|
||||
/**
|
||||
* A method that computes the final action of the quick-fix and returns it. Executed in a background read-action.
|
||||
*
|
||||
* @param project {@link Project}
|
||||
* @param descriptor problem reported by the tool which provided this quick fix action
|
||||
* @return a command to be applied to finally execute the fix.
|
||||
* @see ModCommands for useful utility methods to construct the commands
|
||||
*/
|
||||
@NotNull
|
||||
public abstract ModCommand perform(@NotNull Project project, @NotNull ProblemDescriptor descriptor);
|
||||
|
||||
@Override
|
||||
public final boolean startInWriteAction() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final @Nullable PsiElement getElementToMakeWritable(@NotNull PsiFile currentFile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final @Nullable FileModifier getFileModifierForPreview(@NotNull PsiFile target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
|
||||
ModCommand command = getCommand(project, descriptor);
|
||||
if (command.prepare() != ModStatus.SUCCESS) return;
|
||||
command.execute(project);
|
||||
}
|
||||
|
||||
private @NotNull ModCommand getCommand(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
|
||||
ModCommand command;
|
||||
try {
|
||||
command = ReadAction.nonBlocking(() -> SideEffectGuard.computeWithoutSideEffects(() -> perform(project, descriptor)))
|
||||
.submit(AppExecutorUtil.getAppExecutorService()).get();
|
||||
}
|
||||
catch (InterruptedException | ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (ContainerUtil.exists(command.unpack(), c -> c instanceof ModLegacyBridge)) {
|
||||
throw new AssertionError("Wrong fix implementation: " + getClass().getName());
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull IntentionPreviewInfo generatePreview(@NotNull Project project, @NotNull ProblemDescriptor previewDescriptor) {
|
||||
IntentionPreviewInfo info = null;
|
||||
IntentionPreviewInfo info2 = IntentionPreviewInfo.EMPTY;
|
||||
for (ModCommand command : getCommand(project, previewDescriptor).unpack()) {
|
||||
if (command instanceof ModUpdatePsiFile modFile) {
|
||||
if (info != null) {
|
||||
return IntentionPreviewInfo.EMPTY;
|
||||
}
|
||||
PsiFile file = previewDescriptor.getPsiElement().getContainingFile();
|
||||
if (file == modFile.file()) {
|
||||
modFile.execute(project);
|
||||
info = IntentionPreviewInfo.DIFF;
|
||||
} else {
|
||||
info = new IntentionPreviewInfo.CustomDiff(modFile.file().getFileType(), modFile.file().getName(), modFile.oldText(),
|
||||
modFile.newText());
|
||||
}
|
||||
}
|
||||
else if (command instanceof ModNavigate navigate && navigate.caret() != -1) {
|
||||
PsiFile file = PsiManager.getInstance(project).findFile(navigate.file());
|
||||
if (file != null) {
|
||||
info2 = IntentionPreviewInfo.navigate(file, navigate.caret());
|
||||
}
|
||||
}
|
||||
}
|
||||
return info == null ? info2 : info;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.codeInspection;
|
||||
|
||||
import com.intellij.modcommand.ModCommand;
|
||||
import com.intellij.modcommand.ModNavigate;
|
||||
import com.intellij.modcommand.ModNothing;
|
||||
import com.intellij.modcommand.ModUpdatePsiFile;
|
||||
import com.intellij.openapi.editor.Document;
|
||||
import com.intellij.openapi.editor.Editor;
|
||||
import com.intellij.openapi.editor.RangeMarker;
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager;
|
||||
import com.intellij.openapi.fileEditor.TextEditor;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.impl.source.PostprocessReformattingAspect;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Utility methods to create commands
|
||||
*
|
||||
* @see ModCommand
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
public final class ModCommands {
|
||||
/**
|
||||
* @return a command that does nothing
|
||||
*/
|
||||
public static @NotNull ModCommand nop() {
|
||||
return ModNothing.NOTHING;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param target element to select
|
||||
* @return a command that selects given element in the editor, assuming that it's opened in the editor
|
||||
*/
|
||||
public static @NotNull ModCommand select(@NotNull PsiElement target) {
|
||||
VirtualFile file = target.getContainingFile().getVirtualFile();
|
||||
TextRange range = target.getTextRange();
|
||||
return new ModNavigate(file, range.getStartOffset(), range.getEndOffset(), range.getStartOffset());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param orig PsiElement to update
|
||||
* @param updater a function that accepts a non-physical copy of the supplied orig element and performs
|
||||
* PSI write operations in background to modify this copy
|
||||
* @return a command that will perform the corresponding update to the original element
|
||||
*/
|
||||
public static @NotNull ModCommand psiUpdate(@NotNull PsiElement orig, @NotNull Consumer<@NotNull PsiElement> updater) {
|
||||
return psiUpdate(orig, (e, ctx) -> updater.accept(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param orig PsiElement to update
|
||||
* @param updater a function that accepts a non-physical copy of the supplied orig element and a context to
|
||||
* perform additional editor operations if necessary; and performs PSI write operations in background
|
||||
* to modify this copy
|
||||
* @return a command that will perform the corresponding update to the original element
|
||||
*/
|
||||
public static @NotNull ModCommand psiUpdate(@NotNull PsiElement orig,
|
||||
@NotNull BiConsumer<@NotNull PsiElement, @NotNull PsiUpdateContext> updater) {
|
||||
PsiFile origFile = orig.getContainingFile();
|
||||
VirtualFile origVirtualFile = origFile.getOriginalFile().getVirtualFile();
|
||||
Project project = origFile.getProject();
|
||||
Editor origEditor =
|
||||
origVirtualFile != null && FileEditorManager.getInstance(project).getSelectedEditor(origVirtualFile) instanceof TextEditor textEditor
|
||||
?
|
||||
textEditor.getEditor()
|
||||
: null;
|
||||
PsiFile copyFile = (PsiFile)origFile.copy();
|
||||
PsiElement copy = PsiTreeUtil.findSameElementInCopy(orig, copyFile);
|
||||
PostprocessReformattingAspect aspect = PostprocessReformattingAspect.getInstance(project);
|
||||
PsiDocumentManager manager = PsiDocumentManager.getInstance(project);
|
||||
Document document = copyFile.getViewProvider().getDocument();
|
||||
var context = new PsiUpdateContext() {
|
||||
@Nullable RangeMarker mySelectionRange =
|
||||
origEditor == null ? null :
|
||||
document.createRangeMarker(origEditor.getSelectionModel().getSelectionStart(), origEditor.getSelectionModel().getSelectionEnd(),
|
||||
true);
|
||||
@Nullable RangeMarker myCaretRange =
|
||||
origEditor == null ? null :
|
||||
document.createRangeMarker(origEditor.getCaretModel().getOffset(), origEditor.getCaretModel().getOffset(), true);
|
||||
|
||||
@Override
|
||||
public void select(@NotNull PsiElement element) {
|
||||
validate(element);
|
||||
manager.doPostponedOperationsAndUnblockDocument(document);
|
||||
if (mySelectionRange != null) {
|
||||
mySelectionRange.dispose();
|
||||
}
|
||||
if (myCaretRange != null) {
|
||||
myCaretRange.dispose();
|
||||
}
|
||||
mySelectionRange = document.createRangeMarker(element.getTextRange());
|
||||
myCaretRange = document.createRangeMarker(element.getTextRange().getStartOffset(), element.getTextRange().getStartOffset());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveTo(@NotNull PsiElement element) {
|
||||
validate(element);
|
||||
manager.doPostponedOperationsAndUnblockDocument(document);
|
||||
if (myCaretRange != null) {
|
||||
myCaretRange.dispose();
|
||||
}
|
||||
myCaretRange = document.createRangeMarker(element.getTextRange().getStartOffset(), element.getTextRange().getStartOffset());
|
||||
}
|
||||
|
||||
private void validate(@NotNull PsiElement element) {
|
||||
if (!element.isValid()) throw new IllegalArgumentException();
|
||||
if (!PsiTreeUtil.isAncestor(copyFile, element, false)) throw new IllegalArgumentException();
|
||||
}
|
||||
};
|
||||
aspect.postponeFormattingInside(
|
||||
() -> aspect.forcePostprocessFormatInside(copyFile, () -> updater.accept(copy, context)));
|
||||
manager.commitDocument(document);
|
||||
manager.doPostponedOperationsAndUnblockDocument(document);
|
||||
String oldText = origFile.getText();
|
||||
String newText = copyFile.getText();
|
||||
ModCommand command = oldText.equals(newText) ? new ModNothing() : new ModUpdatePsiFile(origFile, oldText, newText);
|
||||
if (origVirtualFile != null) {
|
||||
int start = -1, end = -1, caret = -1;
|
||||
if (context.mySelectionRange != null && context.mySelectionRange.getEndOffset() <= newText.length()) {
|
||||
start = context.mySelectionRange.getStartOffset();
|
||||
end = context.mySelectionRange.getEndOffset();
|
||||
}
|
||||
if (context.myCaretRange != null && context.myCaretRange.getStartOffset() <= newText.length()) {
|
||||
caret = context.myCaretRange.getStartOffset();
|
||||
}
|
||||
if (start != -1 || end != -1 || caret != -1) {
|
||||
command = command.andThen(new ModNavigate(origVirtualFile, start, end, caret));
|
||||
}
|
||||
}
|
||||
if (context.mySelectionRange != null) {
|
||||
context.mySelectionRange.dispose();
|
||||
}
|
||||
if (context.myCaretRange != null) {
|
||||
context.myCaretRange.dispose();
|
||||
}
|
||||
return command;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.codeInspection;
|
||||
|
||||
import com.intellij.psi.PsiElement;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* A helper to perform editor command when building the {@link com.intellij.modcommand.ModCommand}
|
||||
*
|
||||
* @see ModCommands#psiUpdate(PsiElement, BiConsumer)
|
||||
*/
|
||||
public interface PsiUpdateContext {
|
||||
/**
|
||||
* Selects given element
|
||||
*
|
||||
* @param element element to select
|
||||
*/
|
||||
void select(@NotNull PsiElement element);
|
||||
|
||||
/**
|
||||
* Navigates to a given element
|
||||
*
|
||||
* @param element element to navigate to
|
||||
*/
|
||||
void moveTo(@NotNull PsiElement element);
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
// Copyright 2000-2019 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.
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
|
||||
package com.intellij.codeInspection.actions;
|
||||
|
||||
@@ -84,7 +84,7 @@ public class CleanupInspectionIntention implements IntentionAction, HighPriority
|
||||
@Override
|
||||
public boolean isAvailable(@NotNull final Project project, final Editor editor, final PsiFile file) {
|
||||
return myQuickfix.getClass() != EmptyIntentionAction.class &&
|
||||
(myQuickfix.startInWriteAction() || myQuickfix instanceof BatchQuickFix) &&
|
||||
(myQuickfix.startInWriteAction() || myQuickfix instanceof BatchQuickFix || myQuickfix instanceof ModCommandQuickFix) &&
|
||||
editor != null &&
|
||||
!(myToolWrapper instanceof LocalInspectionToolWrapper && ((LocalInspectionToolWrapper)myToolWrapper).isUnfair());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user