IJPL-245860 [mod-completion] IOOBE: DocumentImpl.deleteString via PsiUpdateCompletionItem in injection

Support ModCompletion in injection
(cherry picked from commit 41985f580fe3f4e6155b3ebac160a840de8632b3)
Review ID: IJ-CR-209545

GitOrigin-RevId: 1bd7ea7c17324f78ebf8dd3254f29c952382a3a4
This commit is contained in:
Tagir Valeev
2026-06-25 16:25:07 +00:00
committed by intellij-monorepo-bot
parent 0089a1fc18
commit 49f18225c5
18 changed files with 103 additions and 73 deletions
@@ -82,10 +82,9 @@ public class InstanceofExpressionPostfixTemplate extends PostfixTemplate impleme
public PostfixModExpander createModExpander() {
return (ActionContext actionContext, PostfixTemplateProvider provider, TextRange keyRange) -> {
TextRange selection = actionContext.selection();
return ModCommand.psiUpdate(actionContext.withSelection(new TextRange(keyRange.getStartOffset(), keyRange.getStartOffset()))
.withOffset(keyRange.getStartOffset()),
document -> document.deleteString(selection.getStartOffset(), selection.getEndOffset()),
return ModCommand.psiUpdate(actionContext, true,
updater -> {
updater.select(TextRange.from(keyRange.getStartOffset(), 0));
updater.getDocument().deleteString(PostfixLiveTemplate.positiveOffset(keyRange.getStartOffset()), selection.getStartOffset());
PsiDocumentManager.getInstance(updater.getProject()).commitDocument(updater.getDocument());
PsiElement context =
@@ -137,13 +137,11 @@ public class IntroduceFieldPostfixTemplate extends PostfixTemplateWithExpression
@NotNull PsiExpression virtualExpr,
@NotNull JavaIntroduceFieldService.InitializationPlace place,
@NotNull PostfixTemplateProvider provider) {
TextRange newSelection = new TextRange(keyRange.getStartOffset(), keyRange.getStartOffset());
ActionContext updatedContext = ctx.withSelection(newSelection).withOffset(keyRange.getStartOffset());
JavaIntroduceFieldService introduceFieldService = JavaIntroduceFieldService.getInstance();
if (introduceFieldService == null) return ModCommand.error(JavaRefactoringBundle.message("selected.expression.cannot.be.extracted"));
return ModCommand.psiUpdate(updatedContext,
document -> document.deleteString(ctx.selection().getStartOffset(), ctx.selection().getEndOffset()),
return ModCommand.psiUpdate(ctx, true,
updater -> {
updater.select(TextRange.from(keyRange.getStartOffset(), 0));
updater.getDocument()
.deleteString(PostfixLiveTemplate.positiveOffset(keyRange.getStartOffset()),
ctx.selection().getStartOffset());
@@ -130,13 +130,10 @@ public class IntroduceVariablePostfixTemplate extends PostfixTemplateWithExpress
@NotNull PsiElement virtualExpr,
boolean replaceAll,
@NotNull PostfixTemplateProvider provider) {
TextRange newSelection = new TextRange(keyRange.getStartOffset(), keyRange.getStartOffset());
ActionContext updatedContext = ctx.withSelection(newSelection).withOffset(keyRange.getStartOffset());
ModCommand command = ModCommand.psiUpdate(updatedContext,
document -> {
document.deleteString(ctx.selection().getStartOffset(), ctx.selection().getEndOffset());
},
ModCommand command = ModCommand.psiUpdate(ctx,
true,
updater -> {
updater.select(TextRange.from(keyRange.getStartOffset(), 0));
updater.getDocument()
.deleteString(PostfixLiveTemplate.positiveOffset(keyRange.getStartOffset()), ctx.selection().getStartOffset());
PsiDocumentManager.getInstance(ctx.project()).commitDocument(updater.getDocument());
@@ -55,9 +55,7 @@ public class TryStatementPostfixTemplate extends PostfixTemplate implements Dumb
@Override
public PostfixModExpander createModExpander() {
return (ActionContext actionContext, PostfixTemplateProvider provider, TextRange keyRange) ->
ModCommand.psiUpdate(actionContext.withSelection(new TextRange(keyRange.getStartOffset(), keyRange.getStartOffset()))
.withOffset(keyRange.getStartOffset()),
document -> document.deleteString(actionContext.selection().getStartOffset(), actionContext.selection().getEndOffset()),
ModCommand.psiUpdate(actionContext, true,
updater -> expandModImpl(actionContext, provider, keyRange, updater));
}
@@ -74,10 +74,9 @@ public class TryWithResourcesPostfixTemplate extends PostfixTemplate implements
@Override
public PostfixModExpander createModExpander() {
return (ActionContext actionContext, PostfixTemplateProvider provider, TextRange keyRange) ->
ModCommand.psiUpdate(actionContext.withSelection(new TextRange(keyRange.getStartOffset(), keyRange.getStartOffset()))
.withOffset(keyRange.getStartOffset()),
document -> document.deleteString(actionContext.selection().getStartOffset(), actionContext.selection().getEndOffset()),
ModCommand.psiUpdate(actionContext, true,
updater -> {
updater.select(TextRange.from(keyRange.getStartOffset(), 0));
updater.getDocument().deleteString(PostfixLiveTemplate.positiveOffset(keyRange.getStartOffset()), actionContext.selection().getStartOffset());
PsiDocumentManager.getInstance(actionContext.project()).commitDocument(updater.getDocument());
PsiFile file = updater.getPsiFile();
@@ -0,0 +1,9 @@
import org.intellij.lang.annotations.Language;
class Hello {
void test() {
@Language("JAVA") String string = """
publ<caret>
""";
}
}
@@ -0,0 +1,9 @@
import org.intellij.lang.annotations.Language;
class Hello {
void test() {
@Language("JAVA") String string = """
public <caret>
""";
}
}
@@ -413,6 +413,11 @@ public class KeywordCompletionTest extends LightCompletionTestCase {
public void testNoPrimitivesAfterExpressions3() { doTest(); }
public void testNoPrimitivesAfterExpressions4() { doTest(); }
@NeedsIndex.SmartMode(reason = "Smart is necessary for injection")
public void testInInjection() {
doTest();
}
private void doTest() {
configureByTestName();
+1 -1
View File
@@ -1490,7 +1490,7 @@ com.intellij.modcommand.ModCommandService
- a:chooseFileAndPerform(com.intellij.psi.PsiFile,com.intellij.openapi.editor.Editor,com.intellij.modcommand.ModCommandAction,I):com.intellij.modcommand.ModCommandWithContext
- s:getInstance():com.intellij.modcommand.ModCommandService
- a:insertText(com.intellij.modcommand.ActionContext,java.lang.String,Z):com.intellij.modcommand.ModCommand
- a:psiUpdate(com.intellij.modcommand.ActionContext,java.util.function.Consumer,java.util.function.Consumer):com.intellij.modcommand.ModCommand
- a:psiUpdate(com.intellij.modcommand.ActionContext,Z,java.util.function.Consumer):com.intellij.modcommand.ModCommand
- a:unwrap(com.intellij.codeInspection.LocalQuickFix):com.intellij.modcommand.ModCommandAction
- a:updateOption(com.intellij.psi.PsiElement,com.intellij.codeInspection.InspectionProfileEntry,java.util.function.Consumer):com.intellij.modcommand.ModCommand
- a:wrap(com.intellij.modcommand.ModCommandAction):com.intellij.codeInsight.intention.IntentionAction
@@ -241,23 +241,21 @@ public sealed interface ModCommand
*/
static @NotNull ModCommand psiUpdate(@NotNull ActionContext context,
@NotNull Consumer<@NotNull ModPsiUpdater> updater) {
return ModCommandService.getInstance().psiUpdate(context, doc -> {}, updater);
return ModCommandService.getInstance().psiUpdate(context, false, updater);
}
/**
* @param context a context of the original action
* @param copyCleaner a function that updates the document to ignore intermittent changes in the original.
* This could be useful for completion when the prefix is already inserted to the original document,
* but the final command should be applied to the document without the prefix.
* @param updater a function that accepts an updater, so it can query writable copies from it and perform modifications;
* also additional editor operation like caret positioning could be performed
* @param context a context of the original action
* @param deleteSelection if true, a text selected in the action context will be deleted prior to execution
* @param updater a function that accepts an updater, so it can query writable copies from it and perform modifications;
* also additional editor operation like caret positioning could be performed
* @return a command that will perform the corresponding update to the original elements and the editor
*/
@ApiStatus.Internal
static @NotNull ModCommand psiUpdate(@NotNull ActionContext context,
@NotNull Consumer<@NotNull Document> copyCleaner,
boolean deleteSelection,
@NotNull Consumer<@NotNull ModPsiUpdater> updater) {
return ModCommandService.getInstance().psiUpdate(context, copyCleaner, updater);
return ModCommandService.getInstance().psiUpdate(context, deleteSelection, updater);
}
/**
@@ -267,7 +265,7 @@ public sealed interface ModCommand
* @return a command that will perform the corresponding update to the original element
*/
static <E extends PsiElement> @NotNull ModCommand psiUpdate(@NotNull E orig, @NotNull Consumer<@NotNull E> updater) {
return psiUpdate(orig, (e, ctx) -> updater.accept(e));
return psiUpdate(orig, (e, _) -> updater.accept(e));
}
/**
@@ -310,7 +308,7 @@ public sealed interface ModCommand
final @NotNull @IntentionName String title,
@NotNull Function<@NotNull T, @NotNull ModCommand> function,
@NotNull Function<@NotNull T, @NotNull TextRange> range) {
return new PsiBasedModCommandAction<T>(element) {
return new PsiBasedModCommandAction<>(element) {
@Override
protected @NotNull ModCommand perform(@NotNull ActionContext context, @NotNull T element) {
return function.apply(element);
@@ -343,7 +341,7 @@ public sealed interface ModCommand
final @NotNull @IntentionName String title,
@NotNull BiConsumer<@NotNull T, @NotNull ModPsiUpdater> action,
@NotNull Function<@NotNull T, @NotNull TextRange> range) {
return new PsiUpdateModCommandAction<T>(element) {
return new PsiUpdateModCommandAction<>(element) {
@Override
protected void invoke(@NotNull ActionContext context, @NotNull T element, @NotNull ModPsiUpdater updater) {
action.accept(element, updater);
@@ -387,7 +385,7 @@ public sealed interface ModCommand
* @param file a file where we want to navigate
* @param offset an offset in the file before the command is executed
* @param leanRight if true, lean to the right side when the text was inserted right at the caret position
* @return an updated command which tries to navigate inside the specified file, taking into account the modifications inside that file
* @return an updated command that tries to navigate inside the specified file, taking into account the modifications inside that file
*/
@ApiStatus.Experimental
static @NotNull ModCommand moveCaretAfter(@NotNull ModCommand command, @NotNull PsiFile file, int offset, boolean leanRight) {
@@ -6,7 +6,6 @@ import com.intellij.codeInspection.InspectionProfileEntry;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
@@ -57,7 +56,7 @@ public interface ModCommandService {
* Implementation of ModCommand.psiUpdate; should not be used directly.
*/
@NotNull ModCommand psiUpdate(@NotNull ActionContext context,
@NotNull Consumer<@NotNull Document> copyCleaner,
boolean deleteSelection,
@NotNull Consumer<@NotNull ModPsiUpdater> updater);
/**
@@ -37,23 +37,16 @@ public abstract class PsiUpdateCompletionItem<T> implements ModCompletionItem {
@Override
public ModCommand perform(ActionContext actionContext, InsertionContext insertionContext) {
String lookupString = mainLookupString();
int completionStart = actionContext.selection().getStartOffset();
int prefixEnd = actionContext.selection().getEndOffset();
int updatedCaretPos = completionStart + lookupString.length();
ActionContext finalActionContext = actionContext
.withSelection(TextRange.create(updatedCaretPos, updatedCaretPos))
.withOffset(updatedCaretPos);
return ModCommand.psiUpdate(finalActionContext, doc -> {
doc.deleteString(completionStart, prefixEnd);
}, updater -> {
return ModCommand.psiUpdate(actionContext, true, updater -> {
Document document = updater.getDocument();
document.replaceString(completionStart,
String lookupString = mainLookupString();
TextRange range = TextRange.create(updater.getCaretOffset(), updater.getCaretOffset() + lookupString.length());
document.replaceString(range.getStartOffset(),
insertionContext.mode() == InsertionMode.OVERWRITE ?
calculateEndOffsetForOverwrite(document, completionStart) : completionStart, lookupString);
updater.moveCaretTo(updatedCaretPos);
update(actionContext.withOffset(updatedCaretPos)
.withSelection(TextRange.create(completionStart, updatedCaretPos)), insertionContext, updater);
calculateEndOffsetForOverwrite(document, range.getStartOffset()) : range.getStartOffset(), lookupString);
updater.moveCaretTo(range.getEndOffset());
update(actionContext.withOffset(range.getEndOffset())
.withSelection(range), insertionContext, updater);
addCompletionChar(updater, insertionContext);
});
}
@@ -72,9 +72,9 @@ public final class ModCommandServiceImpl implements ModCommandService {
@Override
public @NotNull ModCommand psiUpdate(@NotNull ActionContext context,
@NotNull Consumer<@NotNull Document> copyCleaner,
boolean deleteSelection,
@NotNull Consumer<@NotNull ModPsiUpdater> updater) {
return PsiUpdateImpl.psiUpdate(context, copyCleaner, updater);
return PsiUpdateImpl.psiUpdate(context, deleteSelection, updater);
}
@Override
@@ -82,14 +82,14 @@ final class PsiUpdateImpl {
private static final Key<PsiFile> ORIGINAL_FILE_FOR_INJECTION = Key.create("ORIGINAL_FILE_FOR_INJECTION");
static @NotNull ModCommand psiUpdate(@NotNull ActionContext context,
@NotNull Consumer<@NotNull Document> copyCleaner,
boolean deleteSelection,
@NotNull Consumer<@NotNull ModPsiUpdater> updater) {
var runnable = new Runnable() {
private ModPsiUpdaterImpl myUpdater;
@Override
public void run() {
myUpdater = new ModPsiUpdaterImpl(context, copyCleaner);
myUpdater = new ModPsiUpdaterImpl(context, deleteSelection);
updater.accept(myUpdater);
}
@@ -121,15 +121,13 @@ final class PsiUpdateImpl {
private boolean myDeleted;
private boolean myGuardModification;
FileTracker(@NotNull PsiFile origFile, @NotNull Map<PsiFile, FileTracker> changedFiles, @NotNull Consumer<@NotNull Document> copyCleaner) {
FileTracker(@NotNull PsiFile origFile, @NotNull Map<PsiFile, FileTracker> changedFiles, @Nullable TextRange selectionToDelete) {
Project project = origFile.getProject();
myCopyFile = copyFile(project, origFile);
PsiFileImplUtil.setNonPhysicalFileDeleteHandler(myCopyFile, f -> myDeleted = true);
PsiFileImplUtil.setNonPhysicalFileDeleteHandler(myCopyFile, _ -> myDeleted = true);
assert !myCopyFile.getViewProvider().isEventSystemEnabled() : "Event system for " + myCopyFile.getName();
myManager = PsiDocumentManager.getInstance(project);
myDocument = myCopyFile.getFileDocument();
copyCleaner.accept(myDocument);
myManager.commitDocument(myDocument);
InjectedLanguageManager injectionManager = InjectedLanguageManager.getInstance(project);
boolean injected = injectionManager.isInjectedFragment(origFile);
if (injected) {
@@ -151,6 +149,16 @@ final class PsiUpdateImpl {
}
}, this);
Disposer.register(this, disposable);
if (selectionToDelete != null) {
// The supplied selection is in host-document coordinates (completion uses the top-level editor);
// map it into the injected copy's coordinate space, as myDocument is the copy of the injected file here.
Document injectedDocument = origFile.getFileDocument();
if (injectedDocument instanceof DocumentWindow window) {
int start = injectionManager.mapInjectedOffsetToUnescaped(origFile, window.hostToInjected(selectionToDelete.getStartOffset()));
int end = injectionManager.mapInjectedOffsetToUnescaped(origFile, window.hostToInjected(selectionToDelete.getEndOffset()));
selectionToDelete = TextRange.create(start, end);
}
}
myTargetFile = hostFileCopy;
origFile = hostFile;
myPositionDocument = hostFileCopy.getViewProvider().getDocument();
@@ -160,6 +168,10 @@ final class PsiUpdateImpl {
myTargetFile = myCopyFile;
myPositionDocument = myDocument;
}
if (selectionToDelete != null) {
myDocument.deleteString(selectionToDelete.getStartOffset(), selectionToDelete.getEndOffset());
myManager.commitDocument(myDocument);
}
myPositionDocument.addDocumentListener(this, this);
myOrigText = myTargetFile.getText();
myOrigFile = origFile;
@@ -308,12 +320,12 @@ final class PsiUpdateImpl {
private int myCaretVirtualEnd;
private @NotNull TextRange mySelection;
private final @NotNull List<@NotNull ModRegisterTabOut> myTabOutCommands = new ArrayList<>();
private final Consumer<@NotNull Document> myCopyCleaner;
private final boolean myDeleteSelection;
private final List<ModHighlight.HighlightInfo> myHighlightInfos = new ArrayList<>();
private final List<ModStartTemplate.TemplateField> myTemplateFields = new ArrayList<>();
private final Map<String, Result> myTemplateValues = new HashMap<>();
private final List<ModLaunchEditorAction> myLaunchEditorActions = new ArrayList<>();
private @NotNull Function<? super @NotNull PsiFile, ? extends @NotNull ModCommand> myTemplateFinishFunction = f -> nop();
private @NotNull Function<? super @NotNull PsiFile, ? extends @NotNull ModCommand> myTemplateFinishFunction = _ -> nop();
private @Nullable ModStartRename myRenameSymbol;
private final List<ModUpdateReferences> myTrackedDeclarations = new ArrayList<>();
private boolean myPositionUpdated = false;
@@ -356,11 +368,11 @@ final class PsiUpdateImpl {
}
}
private ModPsiUpdaterImpl(@NotNull ActionContext actionContext, @NotNull Consumer<@NotNull Document> copyCleaner) {
private ModPsiUpdaterImpl(@NotNull ActionContext actionContext, boolean deleteSelection) {
myActionContext = actionContext;
myCaretOffset = myCaretVirtualEnd = actionContext.offset();
mySelection = actionContext.selection();
myCopyCleaner = copyCleaner;
myDeleteSelection = deleteSelection;
}
private @NotNull FileTracker tracker() {
@@ -391,7 +403,15 @@ final class PsiUpdateImpl {
private @NotNull FileTracker tracker(@NotNull PsiFile file) {
FileTracker result = myChangedFiles.computeIfAbsent(file, origFile -> {
var tracker = new FileTracker(origFile, myChangedFiles, myActionContext.file() == file ? myCopyCleaner : doc -> {});
FileTracker tracker;
if (myDeleteSelection && myActionContext.file() == file && mySelection.getLength() > 0) {
tracker = new FileTracker(origFile, myChangedFiles, mySelection);
myCaretOffset = mySelection.getStartOffset();
mySelection = TextRange.from(myCaretOffset, 0);
}
else {
tracker = new FileTracker(origFile, myChangedFiles, null);
}
Disposer.register(this, tracker);
return tracker;
});
@@ -417,7 +437,7 @@ final class PsiUpdateImpl {
if (element instanceof PsiDirectory dir) {
VirtualFile file = dir.getVirtualFile();
if (file instanceof ChangedVirtualDirectory) return element;
ChangedDirectoryInfo directory = myChangedDirectories.computeIfAbsent(file, f -> ChangedDirectoryInfo.create(dir));
ChangedDirectoryInfo directory = myChangedDirectories.computeIfAbsent(file, _ -> ChangedDirectoryInfo.create(dir));
@SuppressWarnings("unchecked") E result = (E)directory.psiDirectory;
return result;
}
@@ -557,7 +577,7 @@ final class PsiUpdateImpl {
TemplateImpl.DummyContext context = new TemplateImpl.DummyContext(range, element, getPsiFile());
Result result = varName == null
? expression.calculateResult(context)
: myTemplateValues.computeIfAbsent(varName, v -> expression.calculateResult(context));
: myTemplateValues.computeIfAbsent(varName, _ -> expression.calculateResult(context));
if (result != null) {
FileTracker tracker = requireNonNull(myTracker); // guarded by getRange call
@@ -5,6 +5,7 @@ import com.intellij.application.options.codeStyle.cache.CodeStyleCachingService;
import com.intellij.lang.Language;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
@@ -114,7 +115,8 @@ public final class CodeStyle {
}
return localOrTempSettings;
}
PsiFile topLevel = InjectedLanguageManager.getInstance(project).getTopLevelFile(file);
InjectedLanguageManager manager = InjectedLanguageManager.getInstance(project);
PsiFile topLevel = ReadAction.computeBlocking(() -> manager.getTopLevelFile(file));
PsiFile settingsFile = getSettingsPsi(topLevel != null ? topLevel : file);
if (settingsFile == null) {
return getSettings(project);
@@ -41,6 +41,7 @@ import com.intellij.ide.PowerSaveMode;
import com.intellij.internal.statistic.service.fus.collectors.UIEventLogger;
import com.intellij.lang.LangBundle;
import com.intellij.lang.Language;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.modcommand.ActionContext;
import com.intellij.modcommand.ModCommand;
import com.intellij.modcompletion.ModCompletionItem;
@@ -749,6 +750,8 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable,
ModCompletionItem.InsertionContext insertionContext = new ModCompletionItem.InsertionContext(
completionChar == REPLACE_SELECT_CHAR ? ModCompletionItem.InsertionMode.OVERWRITE : ModCompletionItem.InsertionMode.INSERT,
completionChar);
PsiFile topLevelFile = InjectedLanguageManager.getInstance(psiFile.getProject()).getTopLevelFile(psiFile);
psiFile = topLevelFile == null ? psiFile : topLevelFile;
ActionContext actionContext = ActionContext.from(editor, psiFile);
ActionContext finalActionContext = actionContext
.withOffset(start)
@@ -1302,6 +1305,8 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable,
// templates whose matcher prefix is "" right after the dot). At that moment the
// "prefix length" is logically meaningless
if (prefixLength < 0) return;
PsiFile topLevelFile = InjectedLanguageManager.getInstance(getProject()).getTopLevelFile(file);
file = topLevelFile == null ? file : topLevelFile;
ActionContext actionContext = ActionContext.from(editor, file);
int start = actionContext.offset() - prefixLength;
// it can happen when an external change
@@ -117,16 +117,15 @@ public class ExpressionSelectorModExpander implements PostfixModExpander {
@NotNull TextRange key,
@NotNull PsiElement virtualExpression,
@NotNull PostfixTemplateProvider provider) {
TextRange selection = new TextRange(key.getStartOffset(), key.getStartOffset());
ActionContext updatedContext = ctx.withSelection(selection).withOffset(key.getStartOffset());
return ModCommand.psiUpdate(updatedContext,
document -> document.deleteString(ctx.selection().getStartOffset(), ctx.selection().getEndOffset()),
return ModCommand.psiUpdate(ctx,
true,
updater -> {
updater.select(TextRange.from(key.getStartOffset(), 0));
updater.getDocument().deleteString(PostfixLiveTemplate.positiveOffset(key.getStartOffset()), ctx.selection().getStartOffset());
PsiDocumentManager.getInstance(ctx.project()).commitDocument(updater.getDocument());
provider.prepareCopyForModCommand(updater.getPsiFile(), PostfixLiveTemplate.positiveOffset(key.getStartOffset()));
PsiElement elementInCopy = PsiTreeUtil.findSameElementInCopy(virtualExpression, updater.getPsiFile());
myExpandAction.expand(updatedContext, updater, elementInCopy);
myExpandAction.expand(ctx, updater, elementInCopy);
});
}
}
@@ -109,9 +109,9 @@ public class EditableTemplateModExpander implements PostfixModExpander {
private @NotNull ModCommand createModCommand(@NotNull ActionContext ctx, @NotNull TextRange key,
@NotNull PsiElement virtualExpression,
@NotNull PostfixTemplateProvider provider) {
return ModCommand.psiUpdate(ctx.withSelection(new TextRange(key.getStartOffset(), key.getStartOffset())).withOffset(key.getStartOffset()),
document -> document.deleteString(ctx.selection().getStartOffset(), ctx.selection().getEndOffset()),
return ModCommand.psiUpdate(ctx, true,
updater -> {
updater.select(TextRange.from(key.getStartOffset(), 0));
updater.getDocument().deleteString(PostfixLiveTemplate.positiveOffset(key.getStartOffset()), ctx.selection().getStartOffset());
PsiDocumentManager.getInstance(ctx.project()).commitDocument(updater.getDocument());
provider.prepareCopyForModCommand(updater.getPsiFile(), PostfixLiveTemplate.positiveOffset(key.getStartOffset()));