From 647205e696085f9c2c69a06bfd1aa49f6afef9f3 Mon Sep 17 00:00:00 2001 From: Tagir Valeev Date: Tue, 25 Apr 2023 18:49:54 +0200 Subject: [PATCH] [mod-command] Support injection in ModCommands#psiUpdate GitOrigin-RevId: 3c191627d242960cb0e8f846529f3a2b5627b5c8 --- .../streamApiCallChains/afterInjection.java | 16 ++++ .../streamApiCallChains/beforeInjection.java | 16 ++++ .../modcommand/ModCommandQuickFix.java | 2 +- .../intellij/codeInspection/ModCommands.java | 86 +++++++++++++++---- .../lang/injection/InjectionEditService.java | 19 ++++ .../impl/InjectionEditServiceImpl.java | 40 +++++++++ .../intention/impl/QuickEditHandler.java | 28 +++--- .../src/META-INF/LangExtensions.xml | 2 + 8 files changed, 179 insertions(+), 30 deletions(-) create mode 100644 java/java-tests/testData/inspection/streamApiCallChains/afterInjection.java create mode 100644 java/java-tests/testData/inspection/streamApiCallChains/beforeInjection.java create mode 100644 platform/lang-api/src/com/intellij/lang/injection/InjectionEditService.java create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/intention/impl/InjectionEditServiceImpl.java diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterInjection.java b/java/java-tests/testData/inspection/streamApiCallChains/afterInjection.java new file mode 100644 index 000000000000..ef9da8befd6c --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterInjection.java @@ -0,0 +1,16 @@ +// "Replace with '.keySet().stream()'" "true-preview" + +import org.intellij.lang.annotations.Language; + +class Test { + public void testAnyMatch() { + @Language("JAVA") + String javaText = "import java.util.*;\n" + + "\n" + + "class X {\n" + + " void test(Map map) {\n" + + " map.keySet().stream().filter(x -> !x.isEmpty()).forEach(s -> System.out.println(\"hello\"+s));\n" + + " }\n" + + "}\n"; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeInjection.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeInjection.java new file mode 100644 index 000000000000..97e4e071ab60 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeInjection.java @@ -0,0 +1,16 @@ +// "Replace with '.keySet().stream()'" "true-preview" + +import org.intellij.lang.annotations.Language; + +class Test { + public void testAnyMatch() { + @Language("JAVA") + String javaText = "import java.util.*;\n" + + "\n" + + "class X {\n" + + " void test(Map map) {\n" + + " map.entrySet().stream().map(Map.Entry::getKey).filter(x -> !x.isEmpty()).forEach(s -> System.out.println(\"hello\"+s));\n" + + " }\n" + + "}\n"; + } +} \ No newline at end of file diff --git a/platform/analysis-api/src/com/intellij/modcommand/ModCommandQuickFix.java b/platform/analysis-api/src/com/intellij/modcommand/ModCommandQuickFix.java index d495dd5a17ae..ccecb8d97fd4 100644 --- a/platform/analysis-api/src/com/intellij/modcommand/ModCommandQuickFix.java +++ b/platform/analysis-api/src/com/intellij/modcommand/ModCommandQuickFix.java @@ -54,8 +54,8 @@ public abstract class ModCommandQuickFix implements LocalQuickFix { @Override public @NotNull IntentionPreviewInfo generatePreview(@NotNull Project project, @NotNull ProblemDescriptor previewDescriptor) { - ModCommand modCommand = perform(project, previewDescriptor); PsiFile file = previewDescriptor.getPsiElement().getContainingFile(); + ModCommand modCommand = perform(project, previewDescriptor); return IntentionPreviewUtils.getModCommandPreview(modCommand, file); } } diff --git a/platform/lang-api/src/com/intellij/codeInspection/ModCommands.java b/platform/lang-api/src/com/intellij/codeInspection/ModCommands.java index 6dadd0503ef3..2cece2de699e 100644 --- a/platform/lang-api/src/com/intellij/codeInspection/ModCommands.java +++ b/platform/lang-api/src/com/intellij/codeInspection/ModCommands.java @@ -1,25 +1,31 @@ // 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.lang.Language; import com.intellij.lang.injection.InjectedLanguageManager; +import com.intellij.lang.injection.InjectionEditService; import com.intellij.modcommand.*; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; 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.Disposer; 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.impl.source.tree.injected.EditorWindowTracker; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.LocalTimeCounter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.List; +import java.util.Objects; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -67,20 +73,40 @@ public final class ModCommands { public static @NotNull ModCommand psiUpdate(@NotNull E orig, @NotNull BiConsumer<@NotNull E, @NotNull PsiUpdateContext> updater) { PsiFile origFile = orig.getContainingFile(); - VirtualFile origVirtualFile = origFile.getOriginalFile().getVirtualFile(); Project project = origFile.getProject(); ModCommandAction.ActionContext actionContext = createContext(project, origFile); PsiFile copyFile = actionContext.file(); - String oldText = copyFile.getText(); E copy = PsiTreeUtil.findSameElementInCopy(orig, copyFile); PostprocessReformattingAspect aspect = PostprocessReformattingAspect.getInstance(project); PsiDocumentManager manager = PsiDocumentManager.getInstance(project); Document document = copyFile.getViewProvider().getDocument(); + InjectedLanguageManager injectionManager = InjectedLanguageManager.getInstance(project); + Disposable disposable; + PsiFile targetFile; + Document positionDocument; + boolean injected = injectionManager.isInjectedFragment(origFile); + if (injected) { + PsiLanguageInjectionHost host = Objects.requireNonNull(injectionManager.getInjectionHost(origFile)); + PsiFile hostFile = host.getContainingFile(); + PsiFile hostFileCopy = (PsiFile)hostFile.copy(); + PsiFile injectedFileCopy = getInjectedFileCopy(host, hostFileCopy, orig.getLanguage()); + disposable = ApplicationManager.getApplication().getService(InjectionEditService.class) + .synchronizeWithFragment(injectedFileCopy, document); + targetFile = hostFileCopy; + origFile = hostFile; + positionDocument = hostFileCopy.getViewProvider().getDocument(); + } else { + disposable = null; + targetFile = copyFile; + positionDocument = document; + } + String oldText = targetFile.getText(); var context = new PsiUpdateContext() { @Nullable RangeMarker mySelectionRange = actionContext.selection().getStartOffset() == -1 ? null : - document.createRangeMarker(actionContext.selection().getStartOffset(), actionContext.selection().getEndOffset(), true); + positionDocument.createRangeMarker(actionContext.selection().getStartOffset(), + actionContext.selection().getEndOffset(), true); @Nullable RangeMarker myCaretRange = actionContext.offset() == -1 ? null : - document.createRangeMarker(actionContext.offset(), actionContext.offset(), true); + positionDocument.createRangeMarker(actionContext.offset(), actionContext.offset(), true); @Override public void select(@NotNull PsiElement element) { @@ -92,8 +118,11 @@ public final class ModCommands { if (myCaretRange != null) { myCaretRange.dispose(); } - mySelectionRange = document.createRangeMarker(element.getTextRange()); - myCaretRange = document.createRangeMarker(element.getTextRange().getStartOffset(), element.getTextRange().getStartOffset()); + if (injected) { + element = PsiTreeUtil.findSameElementInCopy(element, targetFile); + } + mySelectionRange = positionDocument.createRangeMarker(element.getTextRange()); + myCaretRange = positionDocument.createRangeMarker(element.getTextRange().getStartOffset(), element.getTextRange().getStartOffset()); } @Override @@ -103,7 +132,10 @@ public final class ModCommands { if (myCaretRange != null) { myCaretRange.dispose(); } - myCaretRange = document.createRangeMarker(element.getTextRange().getStartOffset(), element.getTextRange().getStartOffset()); + if (injected) { + element = PsiTreeUtil.findSameElementInCopy(element, targetFile); + } + myCaretRange = positionDocument.createRangeMarker(element.getTextRange().getStartOffset(), element.getTextRange().getStartOffset()); } private void validate(@NotNull PsiElement element) { @@ -115,8 +147,9 @@ public final class ModCommands { () -> aspect.forcePostprocessFormatInside(copyFile, () -> updater.accept(copy, context))); manager.commitDocument(document); manager.doPostponedOperationsAndUnblockDocument(document); - String newText = copyFile.getText(); + String newText = targetFile.getText(); ModCommand command = oldText.equals(newText) ? new ModNothing() : new ModUpdatePsiFile(origFile, oldText, newText); + VirtualFile origVirtualFile = origFile.getOriginalFile().getVirtualFile(); if (origVirtualFile != null) { int start = -1, end = -1, caret = -1; if (context.mySelectionRange != null && context.mySelectionRange.getEndOffset() <= newText.length()) { @@ -136,23 +169,43 @@ public final class ModCommands { if (context.myCaretRange != null) { context.myCaretRange.dispose(); } + if (disposable != null) { + Disposer.dispose(disposable); + } return command; } + private static @NotNull PsiFile getInjectedFileCopy(@NotNull PsiLanguageInjectionHost host, + @NotNull PsiFile hostFileCopy, + @NotNull Language injectedLanguage) { + InjectedLanguageManager injectionManager = InjectedLanguageManager.getInstance(hostFileCopy.getProject()); + PsiLanguageInjectionHost hostCopy = PsiTreeUtil.findSameElementInCopy(host, hostFileCopy); + var visitor = new PsiLanguageInjectionHost.InjectedPsiVisitor() { + private final Language origLanguage = injectedLanguage; + PsiFile injectedFileCopy = null; + + @Override + public void visit(@NotNull PsiFile injectedPsi, @NotNull List places) { + if (injectedPsi.getLanguage() == origLanguage) { + injectedFileCopy = injectedPsi; + } + } + }; + injectionManager.enumerate(hostCopy, visitor); + return Objects.requireNonNull(visitor.injectedFileCopy); + } + private static ModCommandAction.@NotNull ActionContext createContext(Project project, PsiFile origFile) { var manager = InjectedLanguageManager.getInstance(project); - VirtualFile origVirtualFile = origFile.getOriginalFile().getVirtualFile(); + boolean injectedFragment = manager.isInjectedFragment(origFile); + VirtualFile origVirtualFile = (injectedFragment ? manager.getTopLevelFile(origFile) : origFile).getOriginalFile().getVirtualFile(); Editor origEditor = origVirtualFile != null && FileEditorManager.getInstance(project).getSelectedEditor(origVirtualFile) instanceof TextEditor textEditor ? textEditor.getEditor() : null; - boolean injectedFragment = manager.isInjectedFragment(origFile); - int offset = -1, start = -1, end = -1; + int offset = 0, start = 0, end = 0; if (origEditor != null) { - if (injectedFragment) { - origEditor = EditorWindowTracker.getInstance().getEditorForInjectedFile(origEditor, origFile); - } offset = origEditor.getCaretModel().getOffset(); start = origEditor.getSelectionModel().getSelectionStart(); end = origEditor.getSelectionModel().getSelectionEnd(); @@ -164,9 +217,6 @@ public final class ModCommands { copyFile = PsiFileFactory.getInstance(project).createFileFromText( origFile.getName(), origFile.getFileType(), manager.getUnescapedText(origFile), LocalTimeCounter.currentTime(), false); - offset = manager.mapInjectedOffsetToUnescaped(origFile, offset); - start = manager.mapInjectedOffsetToUnescaped(origFile, start); - end = manager.mapInjectedOffsetToUnescaped(origFile, end); } return new ModCommandAction.ActionContext(project, copyFile, offset, TextRange.create(start, end)); } diff --git a/platform/lang-api/src/com/intellij/lang/injection/InjectionEditService.java b/platform/lang-api/src/com/intellij/lang/injection/InjectionEditService.java new file mode 100644 index 000000000000..cbaf24d0b708 --- /dev/null +++ b/platform/lang-api/src/com/intellij/lang/injection/InjectionEditService.java @@ -0,0 +1,19 @@ +// 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.lang.injection; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.editor.Document; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; + +public interface InjectionEditService { + /** + * Synchronizes the content of injectedFile with the content of copyDocument. + * + * @param injectedFile file to apply changes to + * @param copyDocument unescaped non-physical copy of the original injected file, + * where additional changes will be applied. + * @return disposable to dispose when synchronization is not needed anymore + */ + @NotNull Disposable synchronizeWithFragment(@NotNull PsiFile injectedFile, @NotNull Document copyDocument); +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/InjectionEditServiceImpl.java b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/InjectionEditServiceImpl.java new file mode 100644 index 000000000000..bf749edbbbe3 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/InjectionEditServiceImpl.java @@ -0,0 +1,40 @@ +// 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.codeInsight.intention.impl; + +import com.intellij.injected.editor.InjectedFileChangesHandler; +import com.intellij.lang.injection.InjectedLanguageManager; +import com.intellij.lang.injection.InjectionEditService; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.event.DocumentEvent; +import com.intellij.openapi.editor.event.DocumentListener; +import com.intellij.openapi.editor.impl.ImaginaryEditor; +import com.intellij.openapi.editor.impl.event.DocumentEventImpl; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiLanguageInjectionHost; +import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtilBase; +import com.intellij.psi.impl.source.tree.injected.Place; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class InjectionEditServiceImpl implements InjectionEditService { + @Override + public @NotNull Disposable synchronizeWithFragment(@NotNull PsiFile injectedFile, @NotNull Document copyDocument) { + Place shreds = InjectedLanguageUtilBase.getShreds(injectedFile); + Project project = injectedFile.getProject(); + PsiLanguageInjectionHost host = Objects.requireNonNull(InjectedLanguageManager.getInstance(project).getInjectionHost(injectedFile)); + Editor editor = new ImaginaryEditor(project, host.getContainingFile().getViewProvider().getDocument()); + InjectedFileChangesHandler handler = QuickEditHandler.getHandler(injectedFile, editor, shreds, copyDocument); + copyDocument.addDocumentListener(new DocumentListener() { + @Override + public void documentChanged(@NotNull DocumentEvent event) { + handler.commitToOriginal(event); + } + }); + return handler; + } +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/QuickEditHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/QuickEditHandler.java index 09be94c7d998..15401e96905a 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/QuickEditHandler.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/QuickEditHandler.java @@ -151,17 +151,7 @@ public final class QuickEditHandler extends UserDataHolderBase implements Dispos }, this); - InjectedFileChangesHandlerProvider changesHandlerFactory = - InjectedFileChangesHandlerProvider.EP.forLanguage(firstShred.getHost().getLanguage()); - if (changesHandlerFactory != null) { - myEditChangesHandler = changesHandlerFactory.createFileChangesHandler(shreds, editor, myNewDocument, injectedFile); - } - else if (ContainerUtil.or(shreds, it -> InjectionMeta.INJECTION_INDENT.get(it.getHost()) != null)) { - myEditChangesHandler = new IndentAwareInjectedFileChangesHandler(shreds, editor, myNewDocument, injectedFile); - } - else { - myEditChangesHandler = new CommonInjectedFileChangesHandler(shreds, editor, myNewDocument, injectedFile); - } + myEditChangesHandler = getHandler(injectedFile, editor, shreds, myNewDocument); Disposer.register(this, myEditChangesHandler); StreamEx.of(shreds).map(it -> it.getHost()).nonNull().distinct().forEach(h -> { @@ -181,6 +171,22 @@ public final class QuickEditHandler extends UserDataHolderBase implements Dispos myNewDocument.addDocumentListener(this, this); } + static InjectedFileChangesHandler getHandler(@NotNull PsiFile injectedFile, + @NotNull Editor editor, + @NotNull Place shreds, + @NotNull Document document) { + PsiLanguageInjectionHost host = ContainerUtil.getFirstItem(shreds).getHost(); + InjectedFileChangesHandlerProvider changesHandlerFactory = + host == null ? null : InjectedFileChangesHandlerProvider.EP.forLanguage(host.getLanguage()); + if (changesHandlerFactory != null) { + return changesHandlerFactory.createFileChangesHandler(shreds, editor, document, injectedFile); + } + if (ContainerUtil.or(shreds, it -> InjectionMeta.INJECTION_INDENT.get(it.getHost()) != null)) { + return new IndentAwareInjectedFileChangesHandler(shreds, editor, document, injectedFile); + } + return new CommonInjectedFileChangesHandler(shreds, editor, document, injectedFile); + } + private static final Key> QUICK_EDIT_HANDLERS = Key.create("QUICK_EDIT_HANDLERS"); public static @NotNull Set getFragmentEditors(@NotNull PsiLanguageInjectionHost host) { diff --git a/platform/platform-resources/src/META-INF/LangExtensions.xml b/platform/platform-resources/src/META-INF/LangExtensions.xml index b5844e89da0e..8dd85c7b4c91 100644 --- a/platform/platform-resources/src/META-INF/LangExtensions.xml +++ b/platform/platform-resources/src/META-INF/LangExtensions.xml @@ -97,6 +97,8 @@ serviceImplementation="com.intellij.formatting.FormattingProgressTaskFactory"/> +