diff --git a/platform/analysis-api/src/com/intellij/modcommand/ModPsiUpdater.java b/platform/analysis-api/src/com/intellij/modcommand/ModPsiUpdater.java index 853152d32b9e..2b3fb7b895ac 100644 --- a/platform/analysis-api/src/com/intellij/modcommand/ModPsiUpdater.java +++ b/platform/analysis-api/src/com/intellij/modcommand/ModPsiUpdater.java @@ -14,26 +14,39 @@ import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.function.BiConsumer; +import java.util.function.Consumer; /** - * A helper to perform editor command when building the {@link ModCommand} + * A helper to perform editor command when building the {@link ModCommand}. This helper is available inside the consumer provided by + * {@link ModCommand#psiUpdate} overloads. It allows you to retrieve writable copies of physical files, + * and record various navigation and template operations that will appear in the final command. It also has a concept of 'current file', + * which is the file to perform any interactive operations in. By default, the current file is a file where starting element + * is located, or which action context points at, depending on the used {@code psiUpdate} overload. * * @see ModCommand#psiUpdate(PsiElement, BiConsumer) + * @see ModCommand#psiUpdate(ActionContext, Consumer) */ @ApiStatus.Experimental public interface ModPsiUpdater extends ModPsiNavigator { /** - * @param e element to update - * @return a copy of this element inside a writable non-physical file, whose changes are tracked and will be added to the final command. - * If {@code e} is a {@link PsiDirectory}, a non-physical copy is returned, which allows you to create new files inside that directory. + * Returns a copy of this element inside a writable non-physical file, whose changes are tracked and will be added to the final command. + * If {@code element} is a {@link PsiDirectory}, a non-physical copy is returned, which allows you to create new files inside that directory. * Other write operations on the directory may not work. + *

+ * This method must be called before any writes to the returned non-physical file are performed. Otherwise, + * the copy of original element may not exist anymore. It's better to get all the writable copies before doing any writes. + * + * @param element element to update * @param type of the element + * @return a copy of this element inside a writable non-physical file. + * @throws IllegalStateException if the element is located inside the file, whose copy was already modified. */ @Contract("null -> null; !null -> !null") - E getWritable(E e); + E getWritable(E element) throws IllegalStateException; /** - * Highlight given element as a search result + * Highlights given element as a search result. Does nothing when executed non-interactively. + * The current file may be changed if the element is located in the different file. * * @param element element to select */ @@ -42,7 +55,8 @@ public interface ModPsiUpdater extends ModPsiNavigator { } /** - * Highlight given element + * Highlights given element. Does nothing when executed non-interactively. + * The current file may be changed if the element is located in the different file. * * @param element element to select * @param attributesKey attributes to use for highlighting @@ -50,7 +64,7 @@ public interface ModPsiUpdater extends ModPsiNavigator { void highlight(@NotNull PsiElement element, @NotNull TextAttributesKey attributesKey); /** - * Selects given range + * Highlights given range inside the current file. Does nothing when executed non-interactively. * * @param range range to select * @param attributesKey attributes to use for highlighting @@ -58,7 +72,8 @@ public interface ModPsiUpdater extends ModPsiNavigator { void highlight(@NotNull TextRange range, @NotNull TextAttributesKey attributesKey); /** - * Suggest to rename a given element + * Displays the UI to rename a given element. Does nothing when executed non-interactively. + * The current file may be changed if the element is located in the different file. * * @param element element to rename * @param suggestedNames names to suggest (user is free to type any other name as well) @@ -66,7 +81,7 @@ public interface ModPsiUpdater extends ModPsiNavigator { void rename(@NotNull PsiNameIdentifierOwner element, @NotNull List<@NotNull String> suggestedNames); /** - * @return a builder that allows you to create a template + * @return a builder that allows you to create a template. */ @NotNull ModTemplateBuilder templateBuilder(); @@ -84,4 +99,55 @@ public interface ModPsiUpdater extends ModPsiNavigator { * @param message message to display */ void message(@NotNull @NlsContexts.Tooltip String message); + + /** + * Selects given element. Does nothing when executed non-interactively. + * The current file may be changed if the element is located in the different file. + * + * @param element element to select + */ + @Override + void select(@NotNull PsiElement element); + + /** + * Selects given range in the current file. Does nothing when executed non-interactively. + * + * @param range range to select + */ + @Override + void select(@NotNull TextRange range); + + /** + * Navigates to a given offset in the current file. Does nothing when executed non-interactively. + * + * @param offset offset to move to + */ + @Override + void moveTo(int offset); + + /** + * Navigates to a given element. Does nothing when executed non-interactively. + * The current file may be changed if the element is located in the different file. + * + * @param element element to navigate to + */ + @Override + void moveTo(@NotNull PsiElement element); + + /** + * Moves caret to a previous occurrence of character ch in the current file. Do nothing if no such occurrence is found, + * or when executed non-interactively. + * + * @param ch character to find + */ + @Override + void moveToPrevious(char ch); + + /** + * @return current caret offset inside the current file. It may be based on the previous result of {@link #moveTo(int)} + * or similar methods. The initial caret offset is taken from {@link ActionContext} + * if {@link ModCommand#psiUpdate(ActionContext, Consumer)} was used. Otherwise, it's zero. + */ + @Override + int getCaretOffset(); } diff --git a/platform/analysis-impl/src/com/intellij/lang/impl/modcommand/PsiUpdateImpl.java b/platform/analysis-impl/src/com/intellij/lang/impl/modcommand/PsiUpdateImpl.java index eaba5a3623f8..4e72e558d784 100644 --- a/platform/analysis-impl/src/com/intellij/lang/impl/modcommand/PsiUpdateImpl.java +++ b/platform/analysis-impl/src/com/intellij/lang/impl/modcommand/PsiUpdateImpl.java @@ -265,27 +265,27 @@ final class PsiUpdateImpl { } @Override - public E getWritable(E e) { - if (e == null) return null; - if (e instanceof PsiDirectory dir) { + public E getWritable(E element) { + if (element == null) return null; + if (element instanceof PsiDirectory dir) { VirtualFile file = dir.getVirtualFile(); - if (file instanceof ChangedVirtualDirectory) return e; + if (file instanceof ChangedVirtualDirectory) return element; ChangedDirectoryInfo directory = myChangedDirectories.computeIfAbsent(file, f -> ChangedDirectoryInfo.create(dir)); @SuppressWarnings("unchecked") E result = (E)directory.psiDirectory; return result; } - PsiFile file = e.getContainingFile(); + PsiFile file = element.getContainingFile(); if (file.getViewProvider().getVirtualFile() instanceof ChangedVirtualDirectory.AddedVirtualFile) { - return e; + return element; } PsiFile originalFile = file.getOriginalFile(); if (originalFile != file) { FileTracker tracker = tracker(originalFile); if (tracker.myCopyFile == file) { - return e; + return element; } } - return tracker(file).getCopy(e); + return tracker(file).getCopy(element); } @Override