[mod-command] First experimental draft

GitOrigin-RevId: 58558851eb884fb4a288d788f9009a8e4cb133b8
This commit is contained in:
Tagir Valeev
2023-04-21 14:03:51 +00:00
committed by intellij-monorepo-bot
parent c438670358
commit 42d9b29a47
16 changed files with 609 additions and 44 deletions
@@ -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);
}
}
}
@@ -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);
}
@@ -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());
}