From bfe927613c8d40100d89c9100da5702fbc2acf4c Mon Sep 17 00:00:00 2001 From: peter Date: Thu, 11 Jan 2018 13:44:41 +0100 Subject: [PATCH 1/7] tests: pump event queue before checking leaks --- .../src/com/intellij/testFramework/PlatformTestUtil.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/platform/testFramework/src/com/intellij/testFramework/PlatformTestUtil.java b/platform/testFramework/src/com/intellij/testFramework/PlatformTestUtil.java index 596aa2af81ad..9405cb5dcbe3 100644 --- a/platform/testFramework/src/com/intellij/testFramework/PlatformTestUtil.java +++ b/platform/testFramework/src/com/intellij/testFramework/PlatformTestUtil.java @@ -1131,6 +1131,8 @@ public class PlatformTestUtil { cleanupAllProjects(); + UIUtil.dispatchAllInvocationEvents(); + ApplicationImpl application = (ApplicationImpl)getApplication(); System.out.println(application.writeActionStatistics()); System.out.println(ActionUtil.ActionPauses.STAT.statistics()); From a28e5b71e8b71718e5ee305ce0c60f8af5d46b80 Mon Sep 17 00:00:00 2001 From: peter Date: Thu, 11 Jan 2018 13:51:20 +0100 Subject: [PATCH 2/7] remove MinusculeMatcher.MatchingState to speed up and simplify matching it was added to avoid exponential matching, but that can't be reproduced now --- .../psi/codeStyle/MinusculeMatcher.java | 94 ++++--------------- 1 file changed, 16 insertions(+), 78 deletions(-) diff --git a/platform/util/src/com/intellij/psi/codeStyle/MinusculeMatcher.java b/platform/util/src/com/intellij/psi/codeStyle/MinusculeMatcher.java index 9961a190516e..c914e90a975e 100644 --- a/platform/util/src/com/intellij/psi/codeStyle/MinusculeMatcher.java +++ b/platform/util/src/com/intellij/psi/codeStyle/MinusculeMatcher.java @@ -24,7 +24,6 @@ import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.BitSet; import java.util.Iterator; /** @@ -36,13 +35,6 @@ import java.util.Iterator; * @author peter */ public class MinusculeMatcher implements Matcher { - private final ThreadLocal myMatchingState = new ThreadLocal() { - @Override - protected MatchingState initialValue() { - return new MatchingState(); - } - }; - private final char[] myPattern; private final String myHardSeparators; private final NameUtil.MatchingCaseSensitivity myOptions; @@ -258,25 +250,18 @@ public class MinusculeMatcher implements Matcher { return null; } - MatchingState state = myMatchingState.get(); - state.initializeState(isAscii, length); - try { - return matchWildcards(name, 0, 0, state); - } - finally { - state.releaseState(); - } + return matchWildcards(name, 0, 0, isAscii); } /** * After a wildcard (* or space), search for the first non-wildcard pattern character in the name starting from nameIndex - * and try to {@link #matchFragment(String, int, int, MinusculeMatcher.MatchingState)} for it. + * and try to {@link #matchFragment} for it. */ @Nullable private FList matchWildcards(@NotNull String name, int patternIndex, int nameIndex, - MatchingState matchingState) { + boolean isAsciiName) { if (nameIndex < 0) { return null; } @@ -284,7 +269,7 @@ public class MinusculeMatcher implements Matcher { if (patternIndex == myPattern.length) { return FList.emptyList(); } - return matchFragment(name, patternIndex, nameIndex, matchingState); + return matchFragment(name, patternIndex, nameIndex, isAsciiName); } do { @@ -304,12 +289,12 @@ public class MinusculeMatcher implements Matcher { return FList.emptyList(); } - FList ranges = matchFragment(name, patternIndex, nameIndex, matchingState); + FList ranges = matchFragment(name, patternIndex, nameIndex, isAsciiName); if (ranges != null) { return ranges; } - return matchSkippingWords(name, patternIndex, nameIndex, true, matchingState); + return matchSkippingWords(name, patternIndex, nameIndex, true, isAsciiName); } private static boolean isUpperCaseOrDigit(char p) { @@ -318,19 +303,19 @@ public class MinusculeMatcher implements Matcher { /** * Enumerates places in name that could be matched by the pattern at patternIndex position - * and invokes {@link #matchFragment(String, int, int, MinusculeMatcher.MatchingState)} at those candidate positions + * and invokes {@link #matchFragment} at those candidate positions */ @Nullable private FList matchSkippingWords(@NotNull String name, final int patternIndex, int nameIndex, boolean allowSpecialChars, - MatchingState matchingState) { + boolean isAsciiName) { boolean star = isPatternChar(patternIndex - 1, '*'); final char p = myPattern[patternIndex]; while (true) { int nextOccurrence = star || isWordSeparator[patternIndex] ? - indexOfIgnoreCase(name, nameIndex + 1, p, patternIndex, matchingState.isAsciiName) : + indexOfIgnoreCase(name, nameIndex + 1, p, patternIndex, isAsciiName) : indexOfWordStart(name, patternIndex, nameIndex); if (nextOccurrence < 0) { return null; @@ -350,7 +335,7 @@ public class MinusculeMatcher implements Matcher { NameUtil.isWordStart(name, nextOccurrence) || // accept uppercase matching lowercase if the whole prefix is uppercase and case sensitivity allows that !myHasHumps && myOptions != NameUtil.MatchingCaseSensitivity.ALL) { - FList ranges = matchFragment(name, patternIndex, nextOccurrence, matchingState); + FList ranges = matchFragment(name, patternIndex, nextOccurrence, isAsciiName); if (ranges != null) { return ranges; } @@ -368,26 +353,7 @@ public class MinusculeMatcher implements Matcher { private FList matchFragment(@NotNull String name, int patternIndex, int nameIndex, - MatchingState matchingState) { - if (matchingState.hasFailed(patternIndex, nameIndex)) { - return null; - } - - FList result = doMatchFragments(name, patternIndex, nameIndex, matchingState); - if (result == null) { - matchingState.registerFailure(patternIndex, nameIndex); - } - return result; - } - - /** - * Attempts to match an alphanumeric sequence of pattern (starting at patternIndex) - * to some continuous substring of name, starting from nameIndex. - */ - private FList doMatchFragments(String name, - int patternIndex, - int nameIndex, - MatchingState matchingState) { + boolean isAsciiName) { if (!isFirstCharMatching(name, nameIndex, patternIndex)) { return null; } @@ -402,7 +368,7 @@ public class MinusculeMatcher implements Matcher { patternIndex + i < myPattern.length && charEquals(myPattern[patternIndex+i], patternIndex+i, name.charAt(nameIndex + i), ignoreCase)) { if (isUppercasePatternVsLowercaseNameChar(name, patternIndex + i, nameIndex + i)) { - FList ranges = i < minFragment ? null : findUppercaseMatchFurther(name, patternIndex + i, nameIndex + i, matchingState); + FList ranges = i < minFragment ? null : findUppercaseMatchFurther(name, patternIndex + i, nameIndex + i, isAsciiName); if (ranges != null) { return prependRange(ranges, nameIndex, i); } @@ -423,8 +389,8 @@ public class MinusculeMatcher implements Matcher { // it may not succeed with the longest matching fragment, then try shorter matches while (i >= minFragment || isWildcard(patternIndex + i)) { FList ranges = isWildcard(patternIndex + i) ? - matchWildcards(name, patternIndex + i, nameIndex + i, matchingState) : - matchSkippingWords(name, patternIndex + i, nameIndex + i, false, matchingState); + matchWildcards(name, patternIndex + i, nameIndex + i, isAsciiName) : + matchSkippingWords(name, patternIndex + i, nameIndex + i, false, isAsciiName); if (ranges != null) { return prependRange(ranges, nameIndex, i); } @@ -440,9 +406,9 @@ public class MinusculeMatcher implements Matcher { private FList findUppercaseMatchFurther(String name, int patternIndex, int nameIndex, - MatchingState matchingState) { + boolean isAsciiName) { int nextWordStart = indexOfWordStart(name, patternIndex, nameIndex); - return matchWildcards(name, patternIndex, nextWordStart, matchingState); + return matchWildcards(name, patternIndex, nextWordStart, isAsciiName); } private boolean shouldProhibitCaseMismatch(String name, int patternIndex, int nameIndex) { @@ -539,32 +505,4 @@ public class MinusculeMatcher implements Matcher { return "MinusculeMatcher{myPattern=" + new String(myPattern) + ", myOptions=" + myOptions + '}'; } - private static class MatchingState { - private boolean myBusy; - private int myNameLength; - private boolean isAsciiName; - private final BitSet myTable = new BitSet(); - - void initializeState(boolean isAscii, int length) { - assert !myBusy; - myBusy = true; - myNameLength = length; - isAsciiName = isAscii; - myTable.clear(); - } - - void releaseState() { - assert myBusy; - myBusy = false; - } - - void registerFailure(int patternIndex, int nameIndex) { - myTable.set(patternIndex * myNameLength + nameIndex); - } - - boolean hasFailed(int patternIndex, int nameIndex) { - return myTable.get(patternIndex * myNameLength + nameIndex); - } - - } } From 63f6ca1bb4fc501e21dc4f1235a5b852225d9fd1 Mon Sep 17 00:00:00 2001 From: "Maxim.Kolmakov" Date: Thu, 11 Jan 2018 14:32:46 +0100 Subject: [PATCH 3/7] GUI-71 New Maven project: Please make possible to select a specified maven archetype from the list --- .../intellij/testGuiFramework/cellReader/ExtendedCellReaders.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/testGuiFramework/src/com/intellij/testGuiFramework/cellReader/ExtendedCellReaders.kt b/platform/testGuiFramework/src/com/intellij/testGuiFramework/cellReader/ExtendedCellReaders.kt index bd3718399c32..e38cf3f191c2 100644 --- a/platform/testGuiFramework/src/com/intellij/testGuiFramework/cellReader/ExtendedCellReaders.kt +++ b/platform/testGuiFramework/src/com/intellij/testGuiFramework/cellReader/ExtendedCellReaders.kt @@ -46,7 +46,7 @@ class ExtendedJTreeCellReader : BasicJTreeCellReader(), JTreeCellReader { override fun valueAt(tree: JTree, modelValue: Any?): String? { if (modelValue == null) return null - val cellRendererComponent = tree.cellRenderer.getTreeCellRendererComponent(tree, modelValue, false, false, false, 0, false) + val cellRendererComponent = tree.cellRenderer.getTreeCellRendererComponent(tree, modelValue, false, false, true, 0, false) return getValueWithCellRenderer(cellRendererComponent) } From 52fe5acaf9f5772c1715cdf167b428b7fef1a176 Mon Sep 17 00:00:00 2001 From: Egor Ushakov Date: Thu, 11 Jan 2018 16:37:47 +0300 Subject: [PATCH 4/7] EA-114051 - IDIE: StackFrameProxyImpl.getValue --- .../com/intellij/debugger/jdi/StackFrameProxyImpl.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/java/debugger/impl/src/com/intellij/debugger/jdi/StackFrameProxyImpl.java b/java/debugger/impl/src/com/intellij/debugger/jdi/StackFrameProxyImpl.java index 1ed0ec8628f4..b0c6ee3008e8 100644 --- a/java/debugger/impl/src/com/intellij/debugger/jdi/StackFrameProxyImpl.java +++ b/java/debugger/impl/src/com/intellij/debugger/jdi/StackFrameProxyImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2017 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-2018 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. */ /* @@ -306,6 +306,10 @@ public class StackFrameProxyImpl extends JdiProxy implements StackFrameProxy { error = e; clearCaches(); } + catch (InconsistentDebugInfoException ignored) { + clearCaches(); + throw EvaluateExceptionUtil.INCONSISTEND_DEBUG_INFO; + } catch (InternalException e) { if (e.errorCode() == JvmtiError.INVALID_SLOT || e.errorCode() == JvmtiError.ABSENT_INFORMATION) { throw new EvaluateException(DebuggerBundle.message("error.corrupt.debug.info", e.getMessage()), e); @@ -352,10 +356,6 @@ public class StackFrameProxyImpl extends JdiProxy implements StackFrameProxy { StackFrame stackFrame = getStackFrame(); myAllValues = new THashMap<>(stackFrame.getValues(stackFrame.visibleVariables())); } - catch (InconsistentDebugInfoException ignored) { - clearCaches(); - throw EvaluateExceptionUtil.INCONSISTEND_DEBUG_INFO; - } catch (AbsentInformationException e) { throw EvaluateExceptionUtil.createEvaluateException(e); } From 7334670616ac279e1036cf2b580d25b437484b8d Mon Sep 17 00:00:00 2001 From: Alexey Kudravtsev Date: Thu, 11 Jan 2018 16:10:51 +0300 Subject: [PATCH 5/7] do dispose smart pointers on usage view close to relieve GC (even in refactoring preview there we need to first run the refactoring which uses the smart pointers from the usages view and only then dispose the pointers) --- .../intellij/usages/impl/UsageViewTest.java | 4 +- .../intellij/usages/impl/UsageViewImpl.java | 48 ++++++++++++++----- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/platform/platform-tests/testSrc/com/intellij/usages/impl/UsageViewTest.java b/platform/platform-tests/testSrc/com/intellij/usages/impl/UsageViewTest.java index c5fce3ff91a9..9106a7eb0ddb 100644 --- a/platform/platform-tests/testSrc/com/intellij/usages/impl/UsageViewTest.java +++ b/platform/platform-tests/testSrc/com/intellij/usages/impl/UsageViewTest.java @@ -147,8 +147,8 @@ public class UsageViewTest extends LightPlatformCodeInsightFixtureTestCase { assertTrue(usageView.canPerformReRun()); - usageView.doReRun(); - Set usages = usageView.getUsages(); + UsageView newView = usageView.doReRun(); + Set usages = newView.getUsages(); assertEquals(2, usages.size()); } diff --git a/platform/usageView/src/com/intellij/usages/impl/UsageViewImpl.java b/platform/usageView/src/com/intellij/usages/impl/UsageViewImpl.java index a2f3c4c0e971..16155badbedb 100644 --- a/platform/usageView/src/com/intellij/usages/impl/UsageViewImpl.java +++ b/platform/usageView/src/com/intellij/usages/impl/UsageViewImpl.java @@ -32,6 +32,8 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.pom.Navigatable; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiInvalidElementAccessException; +import com.intellij.psi.SmartPointerManager; +import com.intellij.psi.SmartPsiElementPointer; import com.intellij.psi.impl.PsiDocumentManagerBase; import com.intellij.ui.*; import com.intellij.ui.components.JBPanelWithEmptyText; @@ -155,6 +157,7 @@ public class UsageViewImpl implements UsageView { private final UsageViewTreeCellRenderer myUsageViewTreeCellRenderer; private Usage myOriginUsage; @Nullable private Runnable myRerunActivity; + private boolean myDisposeSmartPointersOnClose = true; public UsageViewImpl(@NotNull final Project project, @NotNull UsageViewPresentation presentation, @@ -1043,16 +1046,18 @@ public class UsageViewImpl implements UsageView { doReRun(); } + /** + * @return usage view which will be shown after re-run (either {@code this} if it knows how to re-run itself, or the new created one otherwise) + */ @SuppressWarnings("WeakerAccess") // used in rider - protected void doReRun() { + protected UsageView doReRun() { myChangesDetected = false; - if (myRerunActivity != null) { - myRerunActivity.run(); - } - else { - com.intellij.usages.UsageViewManager.getInstance(getProject()). + if (myRerunActivity == null) { + return com.intellij.usages.UsageViewManager.getInstance(getProject()). searchAndShowUsages(myTargets, myUsageSearcherFactory, true, false, myPresentation, null); } + myRerunActivity.run(); + return this; } private void reset() { @@ -1271,6 +1276,20 @@ public class UsageViewImpl implements UsageView { } myUpdateAlarm.cancelAllRequests(); } + if (myDisposeSmartPointersOnClose) { + disposeSmartPointers(); + } + } + + private void disposeSmartPointers() { + SmartPointerManager pointerManager = SmartPointerManager.getInstance(getProject()); + for (Usage usage : myUsageNodes.keySet()) { + if (usage instanceof UsageInfo2UsageAdapter) { + SmartPsiElementPointer pointer = ((UsageInfo2UsageAdapter)usage).getUsageInfo().getSmartPointer(); + pointerManager.removePointer(pointer); + } + } + myUsageNodes.clear(); } @Override @@ -1866,13 +1885,20 @@ public class UsageViewImpl implements UsageView { return; } + // can't dispose pointers because refactoring might want to re-use the usage infos from the preview + myDisposeSmartPointersOnClose = false; close(); - CommandProcessor.getInstance().executeCommand( - myProject, myProcessRunnable, - myCommandName, - null - ); + try { + CommandProcessor.getInstance().executeCommand( + myProject, myProcessRunnable, + myCommandName, + null + ); + } + finally { + disposeSmartPointers(); + } } } From da89f80d86e95f784ebe832986b21de8f426e8ef Mon Sep 17 00:00:00 2001 From: Alexey Kudravtsev Date: Thu, 11 Jan 2018 16:28:21 +0300 Subject: [PATCH 6/7] incremental reparse of injected fragments --- .../completion/CompletionTestCase.java | 2 + .../intellij/psi/impl/BooleanRunnable.java | 9 + .../psi/impl/DocumentCommitThread.java | 73 +-- .../psi/impl/PsiDocumentManagerBase.java | 67 +-- .../psi/impl/smartPointers/Identikit.java | 12 +- .../psi/impl/smartPointers/MarkerCache.java | 54 +- .../impl/smartPointers/SelfElementInfo.java | 14 + .../smartPointers/SmartPointerTracker.java | 9 +- .../impl/source/text/BlockSupportImpl.java | 66 ++- .../editorActions/TypedHandler.java | 4 + .../common/InjectedLanguageBlockBuilder.java | 20 +- .../psi/impl/PsiDocumentManagerImpl.java | 51 +- .../tree/injected/DocumentWindowImpl.java | 11 +- .../tree/injected/EditorWindowImpl.java | 25 +- .../injected/InjectedLanguageManagerImpl.java | 2 +- .../tree/injected/InjectedLanguageUtil.java | 24 +- .../tree/injected/InjectionRegistrarImpl.java | 539 +++++++++++------- .../source/tree/injected/LeafPatcher.java | 87 +-- .../impl/source/tree/injected/PlaceInfo.java | 54 ++ .../impl/source/tree/injected/ShredImpl.java | 44 +- 20 files changed, 796 insertions(+), 371 deletions(-) create mode 100644 platform/core-impl/src/com/intellij/psi/impl/BooleanRunnable.java create mode 100644 platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/PlaceInfo.java diff --git a/java/testFramework/src/com/intellij/codeInsight/completion/CompletionTestCase.java b/java/testFramework/src/com/intellij/codeInsight/completion/CompletionTestCase.java index a100551c257b..20cd16175246 100644 --- a/java/testFramework/src/com/intellij/codeInsight/completion/CompletionTestCase.java +++ b/java/testFramework/src/com/intellij/codeInsight/completion/CompletionTestCase.java @@ -19,6 +19,7 @@ import com.intellij.codeInsight.daemon.DaemonAnalyzerTestCase; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupManager; import com.intellij.codeInsight.lookup.impl.LookupImpl; +import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil; import com.intellij.testFramework.PlatformTestCase; import com.intellij.util.containers.ContainerUtil; @@ -63,6 +64,7 @@ public abstract class CompletionTestCase extends DaemonAnalyzerTestCase { } protected void complete(final int time) { + PsiDocumentManager.getInstance(myProject).commitAllDocuments(); new CodeCompletionHandlerBase(myType).invokeCompletion(myProject, InjectedLanguageUtil .getEditorForInjectedLanguageNoCommit(myEditor, getFile()), time); diff --git a/platform/core-impl/src/com/intellij/psi/impl/BooleanRunnable.java b/platform/core-impl/src/com/intellij/psi/impl/BooleanRunnable.java new file mode 100644 index 000000000000..f7da90fc3f9f --- /dev/null +++ b/platform/core-impl/src/com/intellij/psi/impl/BooleanRunnable.java @@ -0,0 +1,9 @@ +/* + * Copyright 2000-2018 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. + */ +package com.intellij.psi.impl; + +@FunctionalInterface +public interface BooleanRunnable { + boolean run(); +} diff --git a/platform/core-impl/src/com/intellij/psi/impl/DocumentCommitThread.java b/platform/core-impl/src/com/intellij/psi/impl/DocumentCommitThread.java index d52d8d7032ee..299950966bac 100644 --- a/platform/core-impl/src/com/intellij/psi/impl/DocumentCommitThread.java +++ b/platform/core-impl/src/com/intellij/psi/impl/DocumentCommitThread.java @@ -30,13 +30,13 @@ import com.intellij.pom.tree.TreeAspectEvent; import com.intellij.psi.*; import com.intellij.psi.codeStyle.CodeStyleManager; import com.intellij.psi.impl.source.PsiFileImpl; +import com.intellij.psi.impl.source.text.BlockSupportImpl; import com.intellij.psi.impl.source.text.DiffLog; import com.intellij.psi.impl.source.tree.FileElement; import com.intellij.psi.impl.source.tree.ForeignLeafPsiElement; import com.intellij.psi.impl.source.tree.TreeUtil; import com.intellij.psi.text.BlockSupport; import com.intellij.util.ExceptionUtil; -import com.intellij.util.Processor; import com.intellij.util.SmartList; import com.intellij.util.concurrency.BoundedTaskExecutor; import com.intellij.util.containers.ContainerUtil; @@ -432,9 +432,9 @@ public class DocumentCommitThread implements Runnable, Disposable, DocumentCommi // synchronized to ensure no new similar tasks can start before we hold the document's lock task = createNewTaskAndCancelSimilar(project, document, allFileNodes, SYNC_COMMIT_REASON, TransactionGuard.getInstance().getContextTransaction(), PsiDocumentManager.getInstance(project).getLastCommittedText(document)); - documentLock.lock(); } + documentLock.lock(); try { assert !task.indicator.isCanceled(); Pair result = commitUnderProgress(task, true); @@ -488,7 +488,8 @@ public class DocumentCommitThread implements Runnable, Disposable, DocumentCommi final Document document = task.getDocument(); final Project project = task.project; final PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(project); - final List> finishProcessors = new SmartList<>(); + final List finishProcessors = new SmartList<>(); + List reparseInjectedProcessors = new SmartList<>(); Ref changedRange = new Ref<>(); Runnable runnable = () -> { myApplication.assertReadAccessAllowed(); @@ -519,9 +520,11 @@ public class DocumentCommitThread implements Runnable, Disposable, DocumentCommi PsiFileImpl file = pair.first; if (file.isValid()) { FileASTNode oldFileNode = pair.second; - Processor finishProcessor = doCommit(task, file, oldFileNode, changedRange); - if (finishProcessor != null) { - finishProcessors.add(finishProcessor); + ProperTextRange changedPsiRange = getChangedPsiRange(file, task, document.getImmutableCharSequence()); + if (changedPsiRange != null) { + BooleanRunnable finishProcessor = doCommit(task, file, oldFileNode, changedPsiRange, reparseInjectedProcessors); + ContainerUtil.addIfNotNull(finishProcessors, finishProcessor); + changedRange.set(changedRange.get() == null ? changedPsiRange : changedRange.get().union(changedPsiRange)); } } else { @@ -541,10 +544,7 @@ public class DocumentCommitThread implements Runnable, Disposable, DocumentCommi } } }; - if (synchronously) { - runnable.run(); - } - else if (!myApplication.tryRunReadAction(runnable)) { + if (!myApplication.tryRunReadAction(runnable)) { log(project, "Could not start read action", task, myApplication.isReadAccessAllowed(), Thread.currentThread()); return new Pair<>(null, "Could not start read action"); } @@ -556,15 +556,16 @@ public class DocumentCommitThread implements Runnable, Disposable, DocumentCommi } ProperTextRange range = changedRange.isNull() ? ProperTextRange.create(0, document.getTextLength()) : changedRange.get(); - Runnable result = createEdtRunnable(task, synchronously, finishProcessors, range); + Runnable result = createFinishCommitInEDTRunnable(task, synchronously, finishProcessors, reparseInjectedProcessors, range); return Pair.create(result, null); } @NotNull - private Runnable createEdtRunnable(@NotNull final CommitTask task, - final boolean synchronously, - @NotNull final List> finishProcessors, - @NotNull ProperTextRange changedRange) { + private Runnable createFinishCommitInEDTRunnable(@NotNull final CommitTask task, + final boolean synchronously, + @NotNull List finishProcessors, + @NotNull List reparseInjectedProcessors, + @NotNull ProperTextRange changedRange) { return () -> { myApplication.assertIsDispatchThread(); Document document = task.getDocument(); @@ -580,7 +581,7 @@ public class DocumentCommitThread implements Runnable, Disposable, DocumentCommi } boolean changeStillValid = task.isStillValid(); - boolean success = changeStillValid && documentManager.finishCommit(document, finishProcessors, changedRange, synchronously, task.reason); + boolean success = changeStillValid && documentManager.finishCommit(document, finishProcessors, reparseInjectedProcessors, changedRange, synchronously, task.reason); if (synchronously) { assert success; } @@ -602,10 +603,11 @@ public class DocumentCommitThread implements Runnable, Disposable, DocumentCommi } @NotNull - private Processor handleCommitWithoutPsi(@NotNull final PsiDocumentManagerBase documentManager, + private BooleanRunnable handleCommitWithoutPsi(@NotNull final PsiDocumentManagerBase documentManager, @NotNull final CommitTask task) { - return document -> { + return () -> { log(task.project, "Finishing without PSI", task); + Document document = task.getDocument(); if (!task.isStillValid() || documentManager.getCachedViewProvider(document) != null) { return false; } @@ -721,21 +723,15 @@ public class DocumentCommitThread implements Runnable, Disposable, DocumentCommi } } - // public for Upsource // returns runnable to execute under write action in AWT to finish the commit, updates "outChangedRange" @Nullable - public Processor doCommit(@NotNull final CommitTask task, - @NotNull final PsiFile file, - @NotNull final FileASTNode oldFileNode, - @NotNull Ref outChangedRange) { + public BooleanRunnable doCommit(@NotNull final CommitTask task, + @NotNull final PsiFile file, + @NotNull final FileASTNode oldFileNode, + @NotNull ProperTextRange changedPsiRange, + @NotNull List outReparseInjectedProcessors) { Document document = task.getDocument(); final CharSequence newDocumentText = document.getImmutableCharSequence(); - ProperTextRange changedPsiRange = getChangedPsiRange(file, task, newDocumentText); - if (changedPsiRange == null) { - return null; - } - - outChangedRange.set(outChangedRange.get() == null ? changedPsiRange : outChangedRange.get().union(changedPsiRange)); final Boolean data = document.getUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY); if (data != null) { @@ -743,11 +739,22 @@ public class DocumentCommitThread implements Runnable, Disposable, DocumentCommi file.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, data); } - BlockSupport blockSupport = BlockSupport.getInstance(file.getProject()); - final DiffLog diffLog = blockSupport.reparseRange(file, oldFileNode, changedPsiRange, newDocumentText, task.indicator, task.myLastCommittedText); + BlockSupportImpl blockSupport = (BlockSupportImpl)BlockSupport.getInstance(file.getProject()); + Trinity result = + BlockSupportImpl.reparse(file, oldFileNode, changedPsiRange, newDocumentText, task.indicator, task.myLastCommittedText); + DiffLog diffLog = result.getFirst(); + ASTNode oldRoot = result.getSecond(); + ASTNode newRoot = result.getThird(); - return document1 -> { + PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(task.project); + + List injectedRunnables = + documentManager.reparseChangedInjectedFragments(document, file, changedPsiRange, task.indicator, oldRoot, newRoot); + outReparseInjectedProcessors.addAll(injectedRunnables); + + return () -> { FileViewProvider viewProvider = file.getViewProvider(); + Document document1 = task.getDocument(); if (!task.isStillValid() || ((PsiDocumentManagerBase)PsiDocumentManager.getInstance(file.getProject())).getCachedViewProvider(document1) != viewProvider) { return false; // optimistic locking failed @@ -864,7 +871,7 @@ public class DocumentCommitThread implements Runnable, Disposable, DocumentCommi return ProperTextRange.create(prefix, end); } - public static void doActualPsiChange(@NotNull final PsiFile file, @NotNull final DiffLog diffLog) { + public static void doActualPsiChange(@NotNull PsiFile file, @NotNull DiffLog diffLog) { CodeStyleManager.getInstance(file.getProject()).performActionWithFormatterDisabled((Runnable)() -> { FileViewProvider viewProvider = file.getViewProvider(); synchronized (((AbstractFileViewProvider)viewProvider).getFilePsiLock()) { diff --git a/platform/core-impl/src/com/intellij/psi/impl/PsiDocumentManagerBase.java b/platform/core-impl/src/com/intellij/psi/impl/PsiDocumentManagerBase.java index e7ebc118cae9..f45b905e6c27 100644 --- a/platform/core-impl/src/com/intellij/psi/impl/PsiDocumentManagerBase.java +++ b/platform/core-impl/src/com/intellij/psi/impl/PsiDocumentManagerBase.java @@ -6,6 +6,7 @@ package com.intellij.psi.impl; import com.google.common.annotations.VisibleForTesting; import com.intellij.injected.editor.DocumentWindow; +import com.intellij.lang.ASTNode; import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.*; @@ -316,7 +317,8 @@ public abstract class PsiDocumentManagerBase extends PsiDocumentManager implemen // public for Upsource public boolean finishCommit(@NotNull final Document document, - @NotNull final List> finishProcessors, + @NotNull List finishProcessors, + @NotNull List reparseInjectedProcessors, @NotNull ProperTextRange changedRange, final boolean synchronously, @NotNull final Object reason) { @@ -326,7 +328,7 @@ public abstract class PsiDocumentManagerBase extends PsiDocumentManager implemen Runnable runnable = new DocumentRunnable(document, myProject) { @Override public void run() { - ok[0] = finishCommitInWriteAction(document, finishProcessors, synchronously); + ok[0] = finishCommitInWriteAction(document, finishProcessors, reparseInjectedProcessors, synchronously, false); } }; if (synchronously) { @@ -339,7 +341,7 @@ public abstract class PsiDocumentManagerBase extends PsiDocumentManager implemen if (ok[0]) { // otherwise changes maybe not synced to the document yet, and injectors will crash if (!mySynchronizer.isDocumentAffectedByTransactions(document)) { - InjectedLanguageManager.getInstance(myProject).startRunInjectorsInRange(document, changedRange, synchronously); + //InjectedLanguageManager.getInstance(myProject).startRunInjectorsInRange(document, changedRange, synchronously); } // run after commit actions outside write action runAfterCommitActions(document); @@ -351,13 +353,8 @@ public abstract class PsiDocumentManagerBase extends PsiDocumentManager implemen } protected boolean finishCommitInWriteAction(@NotNull final Document document, - @NotNull final List> finishProcessors, - final boolean synchronously) { - return finishCommitInWriteAction(document, finishProcessors, synchronously, false); - } - - protected boolean finishCommitInWriteAction(@NotNull final Document document, - @NotNull final List> finishProcessors, + @NotNull List finishProcessors, + @NotNull List reparseInjectedProcessors, final boolean synchronously, boolean forceNoPsiCommit) { ApplicationManager.getApplication().assertIsDispatchThread(); @@ -374,11 +371,11 @@ public abstract class PsiDocumentManagerBase extends PsiDocumentManager implemen myIsCommitInProgress = true; boolean success = true; try { - if (viewProvider != null) { - success = commitToExistingPsi(document, finishProcessors, synchronously, virtualFile, viewProvider); + if (viewProvider == null) { + handleCommitWithoutPsi(document); } else { - handleCommitWithoutPsi(document); + success = commitToExistingPsi(document, finishProcessors, reparseInjectedProcessors, synchronously, virtualFile, viewProvider); } } catch (Throwable e) { @@ -396,10 +393,13 @@ public abstract class PsiDocumentManagerBase extends PsiDocumentManager implemen } private boolean commitToExistingPsi(@NotNull Document document, - @NotNull List> finishProcessors, - boolean synchronously, @Nullable VirtualFile virtualFile, @NotNull FileViewProvider viewProvider) { - for (Processor finishRunnable : finishProcessors) { - boolean success = finishRunnable.process(document); + @NotNull List finishProcessors, + @NotNull List reparseInjectedProcessors, + boolean synchronously, + @Nullable VirtualFile virtualFile, + @NotNull FileViewProvider viewProvider) { + for (BooleanRunnable finishRunnable : finishProcessors) { + boolean success = finishRunnable.run(); if (synchronously) { assert success : finishRunnable + " in " + finishProcessors; } @@ -412,6 +412,9 @@ public abstract class PsiDocumentManagerBase extends PsiDocumentManager implemen getSmartPointerManager().updatePointerTargetsAfterReparse(virtualFile); } viewProvider.contentsSynchronized(); + for (BooleanRunnable runnable : reparseInjectedProcessors) { + if (!runnable.run()) return false; + } return true; } @@ -886,12 +889,8 @@ public abstract class PsiDocumentManagerBase extends PsiDocumentManager implemen } // we can end up outside write action here if the document has forUseInNonAWTThread=true - ApplicationManager.getApplication().runWriteAction(new ExternalChangeAction() { - @Override - public void run() { - ((AbstractFileViewProvider)psiFile.getViewProvider()).onContentReload(); - } - }); + ApplicationManager.getApplication().runWriteAction( + (ExternalChangeAction)((AbstractFileViewProvider)psiFile.getViewProvider())::onContentReload); } @Nullable @@ -990,12 +989,7 @@ public abstract class PsiDocumentManagerBase extends PsiDocumentManager implemen public void disableBackgroundCommit(@NotNull Disposable parentDisposable) { assert myPerformBackgroundCommit; myPerformBackgroundCommit = false; - Disposer.register(parentDisposable, new Disposable() { - @Override - public void dispose() { - myPerformBackgroundCommit = true; - } - }); + Disposer.register(parentDisposable, () -> myPerformBackgroundCommit = true); } @Override @@ -1015,7 +1009,7 @@ public abstract class PsiDocumentManagerBase extends PsiDocumentManager implemen } @SuppressWarnings("AssignmentToStaticFieldFromInstanceMethod") - public void reparseFileFromText(PsiFileImpl file) { + public void reparseFileFromText(@NotNull PsiFileImpl file) { ApplicationManager.getApplication().assertIsDispatchThread(); if (isCommitInProgress()) throw new IllegalStateException("Re-entrant commit is not allowed"); @@ -1026,7 +1020,7 @@ public abstract class PsiDocumentManagerBase extends PsiDocumentManager implemen WriteAction.run(() -> { ProgressIndicator indicator = ProgressIndicatorProvider.getGlobalProgressIndicator(); if (indicator == null) indicator = new EmptyProgressIndicator(); - DiffLog log = BlockSupportImpl.makeFullParse(file, node, text, indicator, text); + DiffLog log = BlockSupportImpl.makeFullParse(file, node, text, indicator, text).getFirst(); DocumentCommitThread.doActualPsiChange(file, log); file.getViewProvider().contentsSynchronized(); }); @@ -1042,7 +1036,7 @@ public abstract class PsiDocumentManagerBase extends PsiDocumentManager implemen private final List myEvents = ContainerUtil.newArrayList(); private final ConcurrentMap myFrozenWindows = ContainerUtil.newConcurrentMap(); - private UncommittedInfo(DocumentImpl original) { + private UncommittedInfo(@NotNull DocumentImpl original) { myOriginal = original; myFrozen = original.freeze(); myOriginal.addDocumentListener(this); @@ -1068,4 +1062,13 @@ public abstract class PsiDocumentManagerBase extends PsiDocumentManager implemen } } + @NotNull + List reparseChangedInjectedFragments(@NotNull Document hostDocument, + @NotNull PsiFile hostPsiFile, + @NotNull TextRange range, + @NotNull ProgressIndicator indicator, + @NotNull ASTNode oldRoot, + @NotNull ASTNode newRoot) { + return Collections.emptyList(); + } } diff --git a/platform/core-impl/src/com/intellij/psi/impl/smartPointers/Identikit.java b/platform/core-impl/src/com/intellij/psi/impl/smartPointers/Identikit.java index fe28a9970c76..dd48c77559f8 100644 --- a/platform/core-impl/src/com/intellij/psi/impl/smartPointers/Identikit.java +++ b/platform/core-impl/src/com/intellij/psi/impl/smartPointers/Identikit.java @@ -19,6 +19,7 @@ import com.google.common.base.MoreObjects; import com.intellij.lang.Language; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; +import com.intellij.psi.AbstractFileViewProvider; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.tree.IElementType; @@ -83,9 +84,14 @@ public abstract class Identikit { @Override public PsiElement findPsiElement(@NotNull PsiFile file, int startOffset, int endOffset) { Language actualLanguage = myFileLanguage != Language.ANY ? myFileLanguage : file.getViewProvider().getBaseLanguage(); - PsiElement anchor = file.getViewProvider().findElementAt(startOffset, actualLanguage); - if (anchor == null && startOffset == file.getTextLength()) { - PsiElement lastChild = file.getViewProvider().getPsi(actualLanguage).getLastChild(); + PsiFile actualLanguagePsi = file.getViewProvider().getPsi(actualLanguage); + return findInside(actualLanguagePsi, startOffset, endOffset); + } + + public PsiElement findInside(@NotNull PsiElement element, int startOffset, int endOffset) { + PsiElement anchor = AbstractFileViewProvider.findElementAt(element, startOffset); // finds child in this tree only, unlike PsiElement.findElementAt() + if (anchor == null && startOffset == element.getTextLength()) { + PsiElement lastChild = element.getLastChild(); if (lastChild != null) { anchor = PsiTreeUtil.getDeepestLast(lastChild); } diff --git a/platform/core-impl/src/com/intellij/psi/impl/smartPointers/MarkerCache.java b/platform/core-impl/src/com/intellij/psi/impl/smartPointers/MarkerCache.java index 5d7997076a2a..e18d8db3bdc1 100644 --- a/platform/core-impl/src/com/intellij/psi/impl/smartPointers/MarkerCache.java +++ b/platform/core-impl/src/com/intellij/psi/impl/smartPointers/MarkerCache.java @@ -15,19 +15,25 @@ */ package com.intellij.psi.impl.smartPointers; +import com.intellij.lang.Language; import com.intellij.openapi.editor.event.DocumentEvent; import com.intellij.openapi.editor.impl.FrozenDocument; import com.intellij.openapi.editor.impl.ManualRangeMarker; import com.intellij.openapi.editor.impl.event.DocumentEventImpl; import com.intellij.openapi.editor.impl.event.RetargetRangeMarkers; +import com.intellij.openapi.util.ProperTextRange; +import com.intellij.openapi.util.Segment; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.UnfairTextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; /** * @author peter @@ -158,6 +164,47 @@ class MarkerCache { return updated == null ? null : new UnfairTextRange(updated.getStartOffset(), updated.getEndOffset()); } + @Nullable + static Segment getUpdatedRange(@NotNull PsiFile containingFile, + @NotNull Segment segment, + boolean isSegmentGreedy, + @NotNull FrozenDocument frozen, + @NotNull List events) { + SelfElementInfo info = new SelfElementInfo(containingFile.getProject(), ProperTextRange.create(segment), new Identikit() { + @Nullable + @Override + public PsiElement findPsiElement(@NotNull PsiFile file, int startOffset, int endOffset) { + return null; + } + + @NotNull + @Override + public Language getFileLanguage() { + return null; + } + + @Override + public boolean isForPsiFile() { + return false; + } + }, containingFile, isSegmentGreedy); + List infos = Collections.singletonList(info); + + boolean greedy = info.isGreedy(); + int start = info.getPsiStartOffset(); + int end = info.getPsiEndOffset(); + boolean surviveOnExternalChange = events.stream().anyMatch(event->((DocumentEventImpl)event).getInitialStartOffset() == 0 && ((DocumentEventImpl)event).getInitialOldLength() == frozen.getTextLength()); + ManualRangeMarker marker = new ManualRangeMarker(start, end, greedy, greedy, surviveOnExternalChange, null); + + UpdatedRanges ranges = new UpdatedRanges(0, frozen, infos, new ManualRangeMarker[]{marker}); + // NB: convert events from completion to whole doc change event to more precise translation + List newEvents = + events.stream().map(event -> ((DocumentEventImpl)event).getInitialStartOffset() == 0 && ((DocumentEventImpl)event).getInitialOldLength() == frozen.getTextLength() ? new DocumentEventImpl(event.getDocument(), event.getOffset(), event.getOldFragment(), event.getNewFragment(), event.getOldTimeStamp(), true, ((DocumentEventImpl)event).getInitialStartOffset(), ((DocumentEventImpl)event).getInitialOldLength()) : event) + .collect(Collectors.toList()); + UpdatedRanges updated = applyEvents(newEvents, ranges); + return updated.myMarkers[0]; + } + void rangeChanged() { myUpdatedRanges = null; } @@ -168,9 +215,10 @@ class MarkerCache { private final List mySortedInfos; private final ManualRangeMarker[] myMarkers; - public UpdatedRanges(int eventCount, - FrozenDocument resultDocument, - List sortedInfos, ManualRangeMarker[] markers) { + UpdatedRanges(int eventCount, + @NotNull FrozenDocument resultDocument, + @NotNull List sortedInfos, + @NotNull ManualRangeMarker[] markers) { myEventCount = eventCount; myResultDocument = resultDocument; mySortedInfos = sortedInfos; diff --git a/platform/core-impl/src/com/intellij/psi/impl/smartPointers/SelfElementInfo.java b/platform/core-impl/src/com/intellij/psi/impl/smartPointers/SelfElementInfo.java index 142e38b1329c..c23154fd59b9 100644 --- a/platform/core-impl/src/com/intellij/psi/impl/smartPointers/SelfElementInfo.java +++ b/platform/core-impl/src/com/intellij/psi/impl/smartPointers/SelfElementInfo.java @@ -234,4 +234,18 @@ public class SelfElementInfo extends SmartPointerElementInfo { public String toString() { return "psi:range=" + calcPsiRange() + ",type=" + myIdentikit; } + + public static Segment calcActualRangeAfterDocumentEvents(@NotNull PsiFile containingFile, @NotNull Document document, @NotNull Segment segment, boolean isSegmentGreedy) { + Project project = containingFile.getProject(); + PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(project); + List events = documentManager.getEventsSinceCommit(document); + if (!events.isEmpty()) { + SmartPointerManagerImpl pointerManager = (SmartPointerManagerImpl)SmartPointerManager.getInstance(project); + SmartPointerTracker tracker = pointerManager.getTracker(containingFile.getViewProvider().getVirtualFile()); + if (tracker != null) { + return tracker.getUpdatedRange(containingFile, segment, isSegmentGreedy, (FrozenDocument)documentManager.getLastCommittedDocument(document), events); + } + } + return null; + } } diff --git a/platform/core-impl/src/com/intellij/psi/impl/smartPointers/SmartPointerTracker.java b/platform/core-impl/src/com/intellij/psi/impl/smartPointers/SmartPointerTracker.java index beca357af39f..f57329165b24 100644 --- a/platform/core-impl/src/com/intellij/psi/impl/smartPointers/SmartPointerTracker.java +++ b/platform/core-impl/src/com/intellij/psi/impl/smartPointers/SmartPointerTracker.java @@ -23,6 +23,7 @@ import com.intellij.openapi.util.LowMemoryWatcher; import com.intellij.openapi.util.Segment; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; import com.intellij.util.CommonProcessors; import com.intellij.util.Processor; import com.intellij.util.containers.ContainerUtil; @@ -32,7 +33,9 @@ import org.jetbrains.annotations.TestOnly; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; class SmartPointerTracker { private static final ReferenceQueue ourQueue = new ReferenceQueue<>(); @@ -161,6 +164,10 @@ class SmartPointerTracker { synchronized Segment getUpdatedRange(SelfElementInfo info, FrozenDocument document, List events) { return markerCache.getUpdatedRange(info, document, events); } + @Nullable + synchronized Segment getUpdatedRange(@NotNull PsiFile containingFile, @NotNull Segment segment, boolean isSegmentGreedy, @NotNull FrozenDocument frozen, @NotNull List events) { + return MarkerCache.getUpdatedRange(containingFile, segment, isSegmentGreedy, frozen, events); + } synchronized void switchStubToAst(AnchorElementInfo info, PsiElement element) { info.switchToTreeRange(element); diff --git a/platform/core-impl/src/com/intellij/psi/impl/source/text/BlockSupportImpl.java b/platform/core-impl/src/com/intellij/psi/impl/source/text/BlockSupportImpl.java index 5b3ec7dc1ddc..aed3656102f3 100644 --- a/platform/core-impl/src/com/intellij/psi/impl/source/text/BlockSupportImpl.java +++ b/platform/core-impl/src/com/intellij/psi/impl/source/text/BlockSupportImpl.java @@ -28,10 +28,7 @@ import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.fileTypes.PlainTextLanguage; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.Couple; -import com.intellij.openapi.util.Pair; -import com.intellij.openapi.util.Ref; -import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.*; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.impl.PsiManagerEx; @@ -90,18 +87,48 @@ public class BlockSupportImpl extends BlockSupport { final Couple reparseableRoots = findReparseableRoots(fileImpl, oldFileNode, changedPsiRange, newFileText); return reparseableRoots != null ? mergeTrees(fileImpl, reparseableRoots.first, reparseableRoots.second, indicator, lastCommittedText) - : makeFullParse(fileImpl, oldFileNode, newFileText, indicator, lastCommittedText); + : makeFullParse(fileImpl, oldFileNode, newFileText, indicator, lastCommittedText).getFirst(); } + // return diff log, old node to replace, new node (in dummy file) + @NotNull + public static Trinity reparse(@NotNull final PsiFile file, + @NotNull FileASTNode oldFileNode, + @NotNull TextRange changedPsiRange, + @NotNull final CharSequence newFileText, + @NotNull final ProgressIndicator indicator, + @NotNull CharSequence lastCommittedText) { + PsiFileImpl fileImpl = (PsiFileImpl)file; + + final Couple reparseableRoots = findReparseableRoots(fileImpl, oldFileNode, changedPsiRange, newFileText); + DiffLog diffLog; + ASTNode oldRoot; + ASTNode newRoot; + if (reparseableRoots == null) { + Pair.NonNull pair = makeFullParse(fileImpl, oldFileNode, newFileText, indicator, lastCommittedText); + oldRoot = oldFileNode; + newRoot = pair.getSecond(); + diffLog = pair.getFirst(); + } + else { + oldRoot = reparseableRoots.first; + newRoot = reparseableRoots.second; + diffLog = mergeTrees(fileImpl, oldRoot, newRoot, indicator, lastCommittedText); + } + return Trinity.create(diffLog, oldRoot, newRoot); + } + + /** - * This method searches ast node that could be reparsed incrementally and returns pair of target reparseable node and new replacement node. - * Returns null if there is no any chance to make incremental parsing. + * Find ast node that could be reparsed incrementally + * @return Pair (target reparseable node, new replacement node) + * or {@code null} if can't parse incrementally. */ @Nullable - public Couple findReparseableRoots(@NotNull PsiFileImpl file, - @NotNull FileASTNode oldFileNode, - @NotNull TextRange changedPsiRange, - @NotNull CharSequence newFileText) { + public static Couple findReparseableRoots(@NotNull PsiFileImpl file, + @NotNull FileASTNode oldFileNode, + @NotNull TextRange changedPsiRange, + @NotNull CharSequence newFileText) { Project project = file.getProject(); final FileElement fileElement = (FileElement)oldFileNode; final CharTable charTable = fileElement.getCharTable(); @@ -175,12 +202,13 @@ public class BlockSupportImpl extends BlockSupport { ); } + // returns diff log, new file element @NotNull - public static DiffLog makeFullParse(@NotNull PsiFileImpl fileImpl, - @NotNull FileASTNode oldFileNode, - @NotNull CharSequence newFileText, - @NotNull ProgressIndicator indicator, - @NotNull CharSequence lastCommittedText) { + public static Pair.NonNull makeFullParse(@NotNull PsiFileImpl fileImpl, + @NotNull FileASTNode oldFileNode, + @NotNull CharSequence newFileText, + @NotNull ProgressIndicator indicator, + @NotNull CharSequence lastCommittedText) { if (fileImpl instanceof PsiCodeFragment) { FileElement parent = fileImpl.getTreeElement(); final FileElement holderElement = new DummyHolder(fileImpl.getManager(), fileImpl.getContext()).getTreeElement(); @@ -188,7 +216,7 @@ public class BlockSupportImpl extends BlockSupport { DiffLog diffLog = new DiffLog(); diffLog.appendReplaceFileElement(parent, (FileElement)holderElement.getFirstChildNode()); - return diffLog; + return Pair.createNonNull(diffLog, holderElement); } FileViewProvider viewProvider = fileImpl.getViewProvider(); viewProvider.getLanguages(); @@ -217,7 +245,7 @@ public class BlockSupportImpl extends BlockSupport { DiffLog diffLog = mergeTrees(fileImpl, oldFileElement, newFileElement, indicator, lastCommittedText); ((PsiManagerEx)fileImpl.getManager()).getFileManager().setViewProvider(lightFile, null); - return diffLog; + return Pair.createNonNull(diffLog, newFileElement); } @NotNull @@ -227,7 +255,7 @@ public class BlockSupportImpl extends BlockSupport { PsiFile file = providerCopy.getPsi(language); if (file != null && !(file instanceof PsiFileImpl)) { - throw new RuntimeException("View provider " + viewProvider + " refused to provide PsiFileImpl for " + language + details(providerCopy, viewProvider)); + throw new RuntimeException("View provider " + viewProvider + " refused to provide PsiFileImpl for " + language + details(providerCopy, viewProvider) +" and returned this strange thing instead of PsiFileImpl: "+file +" ("+file.getClass()+")"); } PsiFileImpl newFile = (PsiFileImpl)file; diff --git a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/TypedHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/TypedHandler.java index 34afe37ba345..6d6238817dac 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/TypedHandler.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/TypedHandler.java @@ -232,6 +232,10 @@ public class TypedHandler extends TypedActionHandlerBase { long modificationStampBeforeTyping = editor.getDocument().getModificationStamp(); type(originalEditor, charTyped); AutoHardWrapHandler.getInstance().wrapLineIfNecessary(originalEditor, dataContext, modificationStampBeforeTyping); + + if (editor.isDisposed()) { // can be that injected editor disappear + return; + } if (('(' == charTyped || '[' == charTyped || '{' == charTyped) && CodeInsightSettings.getInstance().AUTOINSERT_PAIR_BRACKET && diff --git a/platform/lang-impl/src/com/intellij/psi/formatter/common/InjectedLanguageBlockBuilder.java b/platform/lang-impl/src/com/intellij/psi/formatter/common/InjectedLanguageBlockBuilder.java index 00a684843a94..23d1ab0b871d 100644 --- a/platform/lang-impl/src/com/intellij/psi/formatter/common/InjectedLanguageBlockBuilder.java +++ b/platform/lang-impl/src/com/intellij/psi/formatter/common/InjectedLanguageBlockBuilder.java @@ -28,6 +28,7 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiLanguageInjectionHost; import com.intellij.psi.codeStyle.CodeStyleSettings; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; @@ -38,13 +39,13 @@ import java.util.List; public abstract class InjectedLanguageBlockBuilder { private static final Logger LOG = Logger.getInstance("#com.intellij.psi.formatter.xml.XmlInjectedLanguageBlockBuilder"); - public Block createInjectedBlock(ASTNode node, - Block originalBlock, + @NotNull + public Block createInjectedBlock(@NotNull ASTNode node, + @NotNull Block originalBlock, Indent indent, int offset, TextRange range, - @Nullable Language language) - { + @NotNull Language language) { return new InjectedLanguageBlockWrapper(originalBlock, offset, range, indent, language); } @@ -94,9 +95,9 @@ public abstract class InjectedLanguageBlockBuilder { } String childText; - if ((injectionHost.getTextLength() == textRange.getEndOffset() && textRange.getStartOffset() == 0) || - (canProcessFragment((childText = injectionHost.getText()).substring(0, textRange.getStartOffset()), injectionHost) && - canProcessFragment(childText.substring(textRange.getEndOffset()), injectionHost))) { + if (injectionHost.getTextLength() == textRange.getEndOffset() && textRange.getStartOffset() == 0 || + canProcessFragment((childText = injectionHost.getText()).substring(0, textRange.getStartOffset()), injectionHost) && + canProcessFragment(childText.substring(textRange.getEndOffset()), injectionHost)) { injectedFile[0] = injectedPsi; injectedRangeInsideHost.set(textRange); prefixLength.set(shred.getPrefix().length()); @@ -105,8 +106,7 @@ public abstract class InjectedLanguageBlockBuilder { }; final PsiElement injectionHostPsi = injectionHost.getPsi(); PsiFile containingFile = injectionHostPsi.getContainingFile(); - InjectedLanguageManager - .getInstance(containingFile.getProject()).enumerateEx(injectionHostPsi, containingFile, false, injectedPsiVisitor); + InjectedLanguageManager.getInstance(containingFile.getProject()).enumerateEx(injectionHostPsi, containingFile, true, injectedPsiVisitor); if (injectedFile[0] != null) { final Language childLanguage = injectedFile[0].getLanguage(); @@ -156,7 +156,7 @@ public abstract class InjectedLanguageBlockBuilder { final FormattingModel childModel = builder.createModel(childPsi, getSettings()); Block original = childModel.getRootBlock(); - if ((original.isLeaf() && !injectedNode.getText().trim().isEmpty()) || !original.getSubBlocks().isEmpty()) { + if (original.isLeaf() && !injectedNode.getText().trim().isEmpty() || !original.getSubBlocks().isEmpty()) { result.add(createInjectedBlock(injectedNode, original, indent, offset, range, childLanguage)); } } diff --git a/platform/lang-impl/src/com/intellij/psi/impl/PsiDocumentManagerImpl.java b/platform/lang-impl/src/com/intellij/psi/impl/PsiDocumentManagerImpl.java index 246de939419f..4e344d4c9921 100644 --- a/platform/lang-impl/src/com/intellij/psi/impl/PsiDocumentManagerImpl.java +++ b/platform/lang-impl/src/com/intellij/psi/impl/PsiDocumentManagerImpl.java @@ -19,6 +19,7 @@ package com.intellij.psi.impl; import com.google.common.annotations.VisibleForTesting; import com.intellij.AppTopics; import com.intellij.injected.editor.DocumentWindow; +import com.intellij.lang.ASTNode; import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ReadAction; @@ -31,10 +32,13 @@ import com.intellij.openapi.editor.ex.DocumentBulkUpdateListener; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileEditor.FileDocumentManagerAdapter; import com.intellij.openapi.fileEditor.impl.FileDocumentManagerImpl; +import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectLocator; import com.intellij.openapi.project.impl.ProjectImpl; import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.Segment; +import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.pom.core.impl.PomModelImpl; @@ -43,8 +47,10 @@ import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.impl.source.PostprocessReformattingAspect; import com.intellij.psi.impl.source.tree.injected.InjectedLanguageManagerImpl; +import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil; +import com.intellij.util.ArrayUtil; import com.intellij.util.FileContentUtil; -import com.intellij.util.Processor; +import com.intellij.util.containers.ContainerUtil; import com.intellij.util.messages.MessageBus; import com.intellij.util.messages.MessageBusConnection; import org.jetbrains.annotations.NonNls; @@ -52,9 +58,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; -import java.util.Collection; -import java.util.Collections; -import java.util.List; +import java.util.*; //todo listen & notifyListeners readonly events? public class PsiDocumentManagerImpl extends PsiDocumentManagerBase implements SettingsSavingComponent { @@ -138,7 +142,7 @@ public class PsiDocumentManagerImpl extends PsiDocumentManagerBase implements Se @VisibleForTesting public void doCommitWithoutReparse(@NotNull Document document) { - finishCommitInWriteAction(document, Collections.emptyList(), true, true); + finishCommitInWriteAction(document, Collections.emptyList(), Collections.emptyList(), true, true); } @Override @@ -150,13 +154,14 @@ public class PsiDocumentManagerImpl extends PsiDocumentManagerBase implements Se @Override protected boolean finishCommitInWriteAction(@NotNull Document document, - @NotNull List> finishProcessors, - boolean synchronously, + @NotNull List finishProcessors, + List reparseInjectedProcessors, + boolean synchronously, boolean forceNoPsiCommit) { if (ApplicationManager.getApplication().isWriteAccessAllowed()) { // can be false for non-physical PSI InjectedLanguageManagerImpl.disposeInvalidEditors(); } - return super.finishCommitInWriteAction(document, finishProcessors, synchronously, forceNoPsiCommit); + return super.finishCommitInWriteAction(document, finishProcessors, reparseInjectedProcessors, synchronously, forceNoPsiCommit); } @Override @@ -191,6 +196,36 @@ public class PsiDocumentManagerImpl extends PsiDocumentManagerBase implements Se ((DocumentCommitThread)myDocumentCommitThread).clearQueue(); } + @NotNull + @Override + List reparseChangedInjectedFragments(@NotNull Document hostDocument, + @NotNull PsiFile hostPsiFile, + @NotNull TextRange hostChangedRange, + @NotNull ProgressIndicator indicator, + @NotNull ASTNode oldRoot, + @NotNull ASTNode newRoot) { + List changedInjected = InjectedLanguageManager.getInstance(myProject).getCachedInjectedDocumentsInRange(hostPsiFile, hostChangedRange); + if (changedInjected.isEmpty()) return Collections.emptyList(); + FileViewProvider hostViewProvider = hostPsiFile.getViewProvider(); + List fromLast = new ArrayList<>(changedInjected); + // make sure modifications do not ruin all document offsets after + fromLast.sort(Collections.reverseOrder(Comparator.comparingInt(doc -> ArrayUtil.getLastElement(doc.getHostRanges()).getEndOffset()))); + List result = new ArrayList<>(changedInjected.size()); + for (DocumentWindow document : fromLast) { + Segment[] ranges = document.getHostRanges(); + if (ranges.length != 0) { + // host document change has left something valid in this document window place. Try to reparse. + PsiFile injectedPsiFile = getCachedPsiFile(document); + if (injectedPsiFile == null || !injectedPsiFile.isValid()) continue; + + BooleanRunnable runnable = InjectedLanguageUtil.reparse(injectedPsiFile, document, hostPsiFile, hostViewProvider, indicator, oldRoot, newRoot); + ContainerUtil.addIfNotNull(result, runnable); + } + } + + return result; + } + @NonNls @Override public String toString() { diff --git a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/DocumentWindowImpl.java b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/DocumentWindowImpl.java index 70ffb43317f9..99ed32e106ab 100644 --- a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/DocumentWindowImpl.java +++ b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/DocumentWindowImpl.java @@ -20,6 +20,7 @@ import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiLanguageInjectionHost; import com.intellij.util.ObjectUtils; import com.intellij.util.Processor; +import com.intellij.util.containers.ContainerUtil; import com.intellij.util.text.ImmutableCharSequence; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -39,9 +40,9 @@ class DocumentWindowImpl extends UserDataHolderBase implements Disposable, Docum private CachedText myCachedText; - DocumentWindowImpl(@NotNull DocumentEx delegate, boolean oneLine, @NotNull Place shreds) { + DocumentWindowImpl(@NotNull DocumentEx delegate, @NotNull Place shreds) { myDelegate = delegate; - myOneLine = oneLine; + myOneLine = ContainerUtil.and(shreds, s->((ShredImpl)s).isOneLine()); synchronized (myLock) { myShreds = shreds; } @@ -660,8 +661,7 @@ class DocumentWindowImpl extends UserDataHolderBase implements Disposable, Docum // heuristics: return offset closest to the caret Editor[] editors = EditorFactory.getInstance().getEditors(getDelegate()); Editor editor = editors.length == 0 ? null : editors[0]; - if (editor != null) - { + if (editor != null) { if (editor instanceof EditorWindow) editor = ((EditorWindow)editor).getDelegate(); int caret = editor.getCaretModel().getOffset(); return Math.abs(caret - offsetInLeftFragment) < Math.abs(caret - offsetInRightFragment) ? offsetInLeftFragment : offsetInRightFragment; @@ -872,8 +872,7 @@ class DocumentWindowImpl extends UserDataHolderBase implements Disposable, Docum Segment hostRange = shred.getHostRangeMarker(); Segment other = otherShred.getHostRangeMarker(); - if (hostRange == null || other == null || hostRange.getStartOffset() != other.getStartOffset()) return false; - if (hostRange.getEndOffset() != other.getEndOffset()) return false; + if (hostRange == null || other == null || !TextRange.areSegmentsEqual(hostRange, other)) return false; } return true; } diff --git a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/EditorWindowImpl.java b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/EditorWindowImpl.java index c52879eaadbb..44ab25401191 100644 --- a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/EditorWindowImpl.java +++ b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/EditorWindowImpl.java @@ -113,14 +113,33 @@ class EditorWindowImpl extends com.intellij.injected.editor.EditorWindowImpl imp while (iterator.hasNext()) { EditorWindowImpl editorWindow = iterator.next(); if (!editorWindow.isValid()) { - editorWindow.dispose(); - - InjectedLanguageUtil.clearCaches(editorWindow.myInjectedFile, editorWindow.getDocument()); + disposeEditor(editorWindow); iterator.remove(); } } } + private static void disposeEditor(@NotNull EditorWindow editorWindow) { + EditorWindowImpl impl = (EditorWindowImpl)editorWindow; + impl.dispose(); + + InjectedLanguageUtil.clearCaches(impl.myInjectedFile, impl.getDocument()); + } + + static void disposeEditorFor(@NotNull DocumentWindow documentWindow) { + synchronized (allEditors) { + for (Iterator iterator = allEditors.iterator(); iterator.hasNext(); ) { + EditorWindowImpl editor = iterator.next(); + if (InjectionRegistrarImpl.intersect(editor.getDocument(), (DocumentWindowImpl)documentWindow)) { + disposeEditor(editor); + iterator.remove(); + break; + } + } + } + } + + @Override public boolean isValid() { return !isDisposed() && !myInjectedFile.getProject().isDisposed() && myInjectedFile.isValid() && myDocumentWindow.isValid(); diff --git a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectedLanguageManagerImpl.java b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectedLanguageManagerImpl.java index a76460ac2e56..2faeb4b772fb 100644 --- a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectedLanguageManagerImpl.java +++ b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectedLanguageManagerImpl.java @@ -420,7 +420,7 @@ public class InjectedLanguageManagerImpl extends InjectedLanguageManager impleme DocumentEx delegate = ((PsiDocumentManagerBase)PsiDocumentManager.getInstance(project)).getLastCommittedDocument(document.getDelegate()); Place place = new Place(); place.addAll(ContainerUtil.map(shreds, shred -> ((ShredImpl)shred).withPsiRange())); - return new DocumentWindowImpl(delegate, document.isOneLine(), place); + return new DocumentWindowImpl(delegate, place); } private static int appendRange(@NotNull List result, int start, int length) { diff --git a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectedLanguageUtil.java b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectedLanguageUtil.java index 9d8596dd32c6..de9ed33fdc7f 100644 --- a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectedLanguageUtil.java +++ b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectedLanguageUtil.java @@ -20,6 +20,7 @@ import com.intellij.extapi.psi.PsiFileBase; import com.intellij.injected.editor.DocumentWindow; import com.intellij.injected.editor.EditorWindow; import com.intellij.injected.editor.VirtualFileWindow; +import com.intellij.lang.ASTNode; import com.intellij.lang.Language; import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.lang.injection.MultiHostRegistrar; @@ -30,11 +31,13 @@ import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.*; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; +import com.intellij.psi.impl.BooleanRunnable; import com.intellij.psi.impl.DebugUtil; import com.intellij.psi.impl.PsiManagerEx; import com.intellij.psi.impl.PsiParameterizedCachedValue; @@ -60,8 +63,8 @@ public class InjectedLanguageUtil { static final Key, TextRange>>> HIGHLIGHT_TOKENS = Key.create("HIGHLIGHT_TOKENS"); public static final Key INJECTED_FRAGMENT_TYPE = Key.create("INJECTED_FRAGMENT_TYPE"); public static final Key FRANKENSTEIN_INJECTION = InjectedLanguageManager.FRANKENSTEIN_INJECTION; - // meaning: injected file text is probably incorrect + // meaning: injected file text is probably incorrect public static void forceInjectionOnElement(@NotNull PsiElement host) { enumerate(host, (injectedPsi, places) -> { }); @@ -723,4 +726,23 @@ public class InjectedLanguageUtil { @NotNull TextRange rangeInsideHost) { ((InjectionRegistrarImpl)registrar).injectReference(language, prefix, suffix, host, rangeInsideHost); } + + // null means failed to reparse + public static BooleanRunnable reparse(@NotNull PsiFile injectedPsiFile, + @NotNull DocumentWindow document, + @NotNull PsiFile hostPsiFile, + @NotNull FileViewProvider hostViewProvider, + @NotNull ProgressIndicator indicator, @NotNull ASTNode oldRoot, @NotNull ASTNode newRoot) { + Language language = injectedPsiFile.getLanguage(); + InjectedFileViewProvider provider = (InjectedFileViewProvider)injectedPsiFile.getViewProvider(); + VirtualFile oldInjectedVFile = provider.getVirtualFile(); + VirtualFile hostVirtualFile = hostViewProvider.getVirtualFile(); + BooleanRunnable runnable = InjectionRegistrarImpl + .reparse(language, (DocumentWindowImpl)document, injectedPsiFile, (VirtualFileWindow)oldInjectedVFile, hostVirtualFile, hostPsiFile, + indicator, oldRoot, newRoot); + if (runnable == null) { + EditorWindowImpl.disposeEditorFor(document); + } + return runnable; + } } diff --git a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectionRegistrarImpl.java b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectionRegistrarImpl.java index bdeb3f513829..51fd80a5628e 100644 --- a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectionRegistrarImpl.java +++ b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectionRegistrarImpl.java @@ -19,40 +19,38 @@ package com.intellij.psi.impl.source.tree.injected; import com.intellij.codeInsight.daemon.impl.DaemonProgressIndicator; import com.intellij.injected.editor.DocumentWindow; import com.intellij.injected.editor.VirtualFileWindow; -import com.intellij.lang.ASTNode; -import com.intellij.lang.Language; -import com.intellij.lang.LanguageParserDefinitions; -import com.intellij.lang.ParserDefinition; +import com.intellij.lang.*; import com.intellij.lang.injection.MultiHostRegistrar; import com.intellij.lexer.Lexer; +import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.ex.DocumentEx; -import com.intellij.openapi.editor.impl.DocumentImpl; import com.intellij.openapi.fileEditor.impl.FileDocumentManagerImpl; import com.intellij.openapi.fileTypes.SyntaxHighlighter; import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory; import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.*; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; +import com.intellij.psi.impl.BooleanRunnable; import com.intellij.psi.impl.DebugUtil; import com.intellij.psi.impl.DocumentCommitThread; import com.intellij.psi.impl.PsiDocumentManagerBase; +import com.intellij.psi.impl.smartPointers.Identikit; +import com.intellij.psi.impl.smartPointers.SelfElementInfo; import com.intellij.psi.impl.smartPointers.SmartPointerManagerImpl; import com.intellij.psi.impl.source.PsiFileImpl; import com.intellij.psi.impl.source.resolve.FileContextUtil; import com.intellij.psi.impl.source.text.BlockSupportImpl; import com.intellij.psi.impl.source.text.DiffLog; import com.intellij.psi.impl.source.tree.FileElement; -import com.intellij.psi.impl.source.tree.LeafElement; -import com.intellij.psi.impl.source.tree.TreeElement; import com.intellij.psi.impl.source.tree.TreeUtil; import com.intellij.psi.injection.ReferenceInjector; import com.intellij.psi.tree.IElementType; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.PsiUtilCore; -import com.intellij.testFramework.LightVirtualFile; import com.intellij.util.ObjectUtils; import com.intellij.util.PathUtil; import com.intellij.util.SmartList; @@ -62,7 +60,6 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; @SuppressWarnings("deprecation") @@ -135,32 +132,6 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost currentThread = null; } - private static class PlaceInfo { - @NotNull private final String prefix; - @NotNull private final String suffix; - @NotNull private final PsiLanguageInjectionHost host; - @NotNull private final TextRange rangeInsideHost; - - PlaceInfo(@NotNull String prefix, - @NotNull String suffix, - @NotNull PsiLanguageInjectionHost host, - @NotNull TextRange rangeInsideHost) { - this.prefix = prefix; - this.suffix = suffix; - this.host = host; - this.rangeInsideHost = rangeInsideHost; - } - - @Override - public String toString() { - return "Shred "+ - (prefix.isEmpty() ? "" : "prefix='"+prefix+"' ") + - (suffix.isEmpty() ? "" : "suffix='"+suffix+"' ") + - "in " + host+ " " + - "in range "+rangeInsideHost; - } - } - @Override @NotNull public MultiHostRegistrar addPlace(@NonNls @Nullable String prefix, @@ -174,7 +145,7 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost } PsiFile containingFile = PsiUtilCore.getTemplateLanguageFile(host); assert containingFile == myHostPsiFile : exceptionContext("Trying to inject into foreign file: "+containingFile, myLanguage, - myHostPsiFile, myHostVirtualFile, myHostDocument, myContextElement, placeInfos); + myHostPsiFile, myHostVirtualFile, myHostDocument, placeInfos); TextRange hostTextRange = host.getTextRange(); if (!hostTextRange.contains(rangeInsideHost.shiftRight(hostTextRange.getStartOffset()))) { clear(); @@ -183,7 +154,9 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost } cleared = false; - PlaceInfo info = new PlaceInfo(ObjectUtils.notNull(prefix, ""), ObjectUtils.notNull(suffix, ""), host, rangeInsideHost); + String nnPrefix = StringUtil.isEmpty(prefix) ? "" : prefix; // intern empty strings too to reduce gc + String nnSuffix = StringUtil.isEmpty(suffix) ? "" : suffix; // intern empty strings too to reduce gc + PlaceInfo info = new PlaceInfo(nnPrefix, nnSuffix, host, rangeInsideHost); placeInfos.add(info); return this; @@ -195,43 +168,44 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost } } - @NotNull - private static Pair.NonNull createShred(@NotNull Project project, @NotNull PlaceInfo info, - @NotNull StringBuilder outChars, - @NotNull PsiFile hostPsiFile) { + private static void decode(@NotNull PlaceInfo info, @NotNull StringBuilder outChars) { int startOffset = outChars.length(); - String prefix = info.prefix; - outChars.append(prefix); - PsiLanguageInjectionHost host = info.host; - LiteralTextEscaper textEscaper = host.createLiteralTextEscaper(); + outChars.append(info.prefix); + LiteralTextEscaper textEscaper = info.myEscaper; - TextRange rangeInsideHost = info.rangeInsideHost; - TextRange relevantRange = textEscaper.getRelevantTextRange().intersection(rangeInsideHost); + TextRange relevantRange = info.getRelevantRangeInsideHost(); if (relevantRange == null) { relevantRange = TextRange.from(textEscaper.getRelevantTextRange().getStartOffset(), 0); } else { int before = outChars.length(); - boolean decodeFailed = !textEscaper.decode(relevantRange, outChars); + boolean decodeSuccessful = textEscaper.decode(relevantRange, outChars); int after = outChars.length(); assert after >= before : "Escaper " + textEscaper + "("+textEscaper.getClass()+") must not mangle char buffer"; - if (decodeFailed) { + if (!decodeSuccessful) { // if there are invalid chars, adjust the range - int offsetInHost = textEscaper.getOffsetInHost(outChars.length() - before, rangeInsideHost); + int offsetInHost = textEscaper.getOffsetInHost(outChars.length() - before, info.registeredRangeInsideHost); relevantRange = relevantRange.intersection(new ProperTextRange(0, offsetInHost)); } } - String suffix = info.suffix; - outChars.append(suffix); + outChars.append(info.suffix); int endOffset = outChars.length(); - TextRange hostTextRange = host.getTextRange(); - TextRange relevantRangeInHost = relevantRange.shiftRight(hostTextRange.getStartOffset()); - SmartPointerManagerImpl manager = (SmartPointerManagerImpl)SmartPointerManager.getInstance(project); - ShredImpl shred = new ShredImpl(manager.createSmartPsiFileRangePointer(hostPsiFile, relevantRangeInHost, true), - manager.createSmartPsiElementPointer(host, hostPsiFile, true), - prefix, suffix, new ProperTextRange(startOffset, endOffset), false, textEscaper.isOneLine()); - return Pair.createNonNull(shred, textEscaper); + info.rangeInDecodedPSI = new ProperTextRange(startOffset, endOffset); + info.rangeInHostElement = relevantRange; + } + @NotNull + private static ShredImpl createShred(@NotNull PlaceInfo info, @NotNull StringBuilder outChars, @NotNull PsiFile hostPsiFile) { + decode(info, outChars); + + TextRange relevantRange = info.rangeInHostElement; + + TextRange hostTextRange = info.host.getTextRange(); + TextRange relevantRangeInHostFile = relevantRange.shiftRight(hostTextRange.getStartOffset()); + SmartPointerManagerImpl manager = (SmartPointerManagerImpl)SmartPointerManager.getInstance(hostPsiFile.getProject()); + return new ShredImpl(manager.createSmartPsiFileRangePointer(hostPsiFile, relevantRangeInHostFile, true), + manager.createSmartPsiElementPointer(info.host, hostPsiFile, true), + info.prefix, info.suffix, info.rangeInDecodedPSI, false, info.myEscaper.isOneLine()); } @Override @@ -244,12 +218,12 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost if (placeInfos.isEmpty()) { throw new IllegalStateException("Seems you haven't called addPlace()"); } + Language forcedLanguage = myContextElement.getUserData(InjectedFileViewProvider.LANGUAGE_FOR_INJECTED_COPY_KEY); + checkForCorrectContextElement(placeInfos, myContextElement, myLanguage, myHostPsiFile, myHostVirtualFile, myHostDocument); + synchronized (InjectedLanguageManagerImpl.ourInjectionPsiLock) { - Language forcedLanguage = myContextElement.getUserData(InjectedFileViewProvider.LANGUAGE_FOR_INJECTED_COPY_KEY); - PsiFile psiFile = - createInjectedFile(myProject, myLanguage, forcedLanguage, - myHostDocument, myHostVirtualFile, myHostPsiFile, fileExtension, placeInfos, - myContextElement); + PsiFile psiFile = createInjectedFile(myLanguage, forcedLanguage, + myHostDocument, myHostVirtualFile, myHostPsiFile, fileExtension, placeInfos); addFileToResults(psiFile); PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(myProject); @@ -262,81 +236,49 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost } } + private static void checkForCorrectContextElement(@NotNull List placeInfos, + @NotNull PsiElement contextElement, + @NotNull Language language, + @NotNull PsiFile hostPsiFile, + @NotNull VirtualFile hostVirtualFile, + @NotNull DocumentEx hostDocument) { + boolean isAncestor = false; + for (PlaceInfo info : placeInfos) { + isAncestor |= PsiTreeUtil.isAncestor(contextElement, info.host, false); + } + assert isAncestor : exceptionContext("Context element " + contextElement.getTextRange() + ": '" + contextElement + "'; " + + " must be the parent of at least one of injection hosts", language, + hostPsiFile, hostVirtualFile, hostDocument, placeInfos); + } + @NotNull - private static PsiFile createInjectedFile(@NotNull Project project, - @NotNull Language language, @Nullable Language forcedLanguage, + private static PsiFile createInjectedFile(@NotNull Language language, @Nullable Language forcedLanguage, @NotNull DocumentEx hostDocument, @NotNull VirtualFile hostVirtualFile, @NotNull PsiFile hostPsiFile, @Nullable String injectedFileExtension, - @NotNull List placeInfos, - @NotNull PsiElement contextElement) { + @NotNull List placeInfos) { + Project project = hostPsiFile.getProject(); + PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(project); + StringBuilder decodedChars = new StringBuilder(); + Place place = new Place(); + for (PlaceInfo info : placeInfos) { + ShredImpl shred = createShred(info, decodedChars, hostPsiFile); + place.add(shred); + info.hostSmartPointer = shred.getSmartPointer(); + } synchronized (InjectedLanguageManagerImpl.ourInjectionPsiLock) { - PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(project); - - StringBuilder decodedChars = new StringBuilder(); - boolean isOneLine = true; - Place place = new Place(); - boolean isAncestor = false; - List escapers = new ArrayList<>(placeInfos.size()); - for (PlaceInfo info : placeInfos) { - Pair.NonNull p = createShred(project, info, decodedChars, hostPsiFile); - ShredImpl shred = p.getFirst(); - isOneLine &= shred.isOneLine(); - place.add(shred); - - isAncestor |= PsiTreeUtil.isAncestor(contextElement, info.host, false); - escapers.add(p.getSecond()); - } - assert isAncestor : exceptionContext(contextElement + " must be the parent of at least one of injection hosts", language, - hostPsiFile, hostVirtualFile, hostDocument, contextElement, placeInfos); - DocumentWindowImpl documentWindow = new DocumentWindowImpl(hostDocument, isOneLine, place); + DocumentWindowImpl documentWindow = new DocumentWindowImpl(hostDocument, place); String fileName = PathUtil.makeFileName(hostVirtualFile.getName(), injectedFileExtension); - VirtualFileWindowImpl virtualFile = new VirtualFileWindowImpl(fileName, hostVirtualFile, documentWindow, language, decodedChars); - Language finalLanguage = forcedLanguage == null ? LanguageSubstitutors.INSTANCE.substituteLanguage(language, virtualFile, project) : forcedLanguage; - createDocument(virtualFile); - - InjectedFileViewProvider viewProvider = new InjectedFileViewProvider(PsiManager.getInstance(project), virtualFile, documentWindow, - finalLanguage); - ParserDefinition parserDefinition = LanguageParserDefinitions.INSTANCE.forLanguage(finalLanguage); - assert parserDefinition != null : "Parser definition for language " + finalLanguage + " is null"; - PsiFile psiFile = parserDefinition.createFile(viewProvider); + ASTNode parsedNode = + parseFile(language, forcedLanguage, documentWindow, hostVirtualFile, hostDocument, hostPsiFile, project, documentWindow.getText(), + placeInfos, decodedChars, fileName); + PsiFile psiFile = (PsiFile)parsedNode.getPsi(); + InjectedFileViewProvider viewProvider = (InjectedFileViewProvider)psiFile.getViewProvider(); SmartPsiElementPointer pointer = ((ShredImpl)place.get(0)).getSmartPointer(); - - final ASTNode parsedNode = keepTreeFromChameleoningBack(psiFile); - - assert parsedNode instanceof FileElement : "Parsed to " + parsedNode + " instead of FileElement"; - - String documentText = documentManager.getLastCommittedDocument(documentWindow).getText(); - assert ((FileElement)parsedNode).textMatches(decodedChars) : exceptionContext("Before patch: doc:\n'" + documentText + "'\n---PSI:\n'" + parsedNode.getText() + "'\n---chars:\n'" + - decodedChars + "'", - finalLanguage, hostPsiFile, hostVirtualFile, - hostDocument, contextElement, placeInfos); - - viewProvider.doNotInterruptMeWhileImPatchingLeaves(() -> { - try { - patchLeaves(parsedNode, place, escapers); - } - catch (ProcessCanceledException e) { - throw e; - } - catch (RuntimeException e) { - throw new RuntimeException(exceptionContext("Patch error", finalLanguage, hostPsiFile, hostVirtualFile, hostDocument, - contextElement, placeInfos), e); - } - }); - if (!((FileElement)parsedNode).textMatches(documentText)) { - throw new AssertionError(exceptionContext("After patch: doc:\n'" + documentText + "'\n---PSI:\n'" + parsedNode.getText() + "'\n---chars:\n'" + - decodedChars + "'", - finalLanguage, hostPsiFile, hostVirtualFile, hostDocument, contextElement, placeInfos)); - } - - virtualFile.setContent(null, documentWindow.getText(), false); - virtualFile.setWritable(virtualFile.getDelegate().isWritable()); - - cacheEverything(place, documentWindow, viewProvider, psiFile, pointer); + cacheEverything(place, documentWindow, viewProvider, psiFile, pointer, documentManager); PsiFile cachedPsiFile = documentManager.getCachedPsiFile(documentWindow); assert cachedPsiFile == psiFile : "Cached psi :"+ cachedPsiFile +" instead of "+psiFile; @@ -352,8 +294,7 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost psiFile = newFile; viewProvider = (InjectedFileViewProvider)psiFile.getViewProvider(); documentWindow = (DocumentWindowImpl)viewProvider.getDocument(); - virtualFile = (VirtualFileWindowImpl)viewProvider.getVirtualFile(); - boolean shredsReused = !cacheEverything(place, documentWindow, viewProvider, psiFile, pointer); + boolean shredsReused = !cacheEverything(place, documentWindow, viewProvider, psiFile, pointer, documentManager); if (shredsReused) { place.dispose(); mergedPlace = documentWindow.getShreds(); @@ -363,28 +304,33 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost assert psiFile.isValid(); assert mergedPlace.isValid(); assert viewProvider.isValid(); - - try { - List, TextRange>> - tokens = obtainHighlightTokensFromLexer(finalLanguage, decodedChars, place, virtualFile, project, escapers); - psiFile.putUserData(InjectedLanguageUtil.HIGHLIGHT_TOKENS, tokens); - } - catch (ProcessCanceledException e) { - throw e; - } - catch (RuntimeException e) { - throw new RuntimeException(exceptionContext("Obtaining tokens error", finalLanguage, hostPsiFile, hostVirtualFile, - hostDocument, contextElement, placeInfos), e); - } return psiFile; } } - public void injectReference(@NotNull Language language, - @NotNull String prefix, - @NotNull String suffix, - @NotNull PsiLanguageInjectionHost host, - @NotNull TextRange rangeInsideHost) { + private static class PatchException extends Exception { + PatchException(String message) { + super(message); + } + } + private static void patchLeaves(@NotNull List placeInfos, + @NotNull InjectedFileViewProvider viewProvider, + @NotNull ASTNode parsedNode, + @NotNull CharSequence documentText) throws PatchException { + viewProvider.doNotInterruptMeWhileImPatchingLeaves(() -> { + LeafPatcher patcher = new LeafPatcher(placeInfos, parsedNode.getTextLength()); + patcher.patch(parsedNode, placeInfos); + }); + if (!((FileElement)parsedNode).textMatches(documentText)) { + throw new PatchException("After patch: doc:\n'" + documentText + "'\n---PSI:\n'" + parsedNode.getText()); + } + } + + void injectReference(@NotNull Language language, + @NotNull String prefix, + @NotNull String suffix, + @NotNull PsiLanguageInjectionHost host, + @NotNull TextRange rangeInsideHost) { ParserDefinition parser = LanguageParserDefinitions.INSTANCE.forLanguage(language); if (parser != null) { throw new IllegalArgumentException("Language "+language+" being injected as reference must not have ParserDefinition and yet - "+parser); @@ -406,8 +352,8 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost addPlace(prefix, suffix, host, rangeInsideHost); Place place = new Place(); StringBuilder decodedChars = new StringBuilder(); - Pair.NonNull p = createShred(myProject, placeInfos.get(0), decodedChars, myHostPsiFile); - place.add(p.getFirst()); + ShredImpl shred = createShred(placeInfos.get(0), decodedChars, myHostPsiFile); + place.add(shred); if (resultReferences == null) { resultReferences = new SmartList<>(); } @@ -415,19 +361,13 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost clear(); } - - private static void createDocument(@NotNull LightVirtualFile virtualFile) { - CharSequence content = virtualFile.getContent(); - DocumentImpl document = new DocumentImpl(content, StringUtil.indexOf(content, '\r') >= 0, false); - FileDocumentManagerImpl.registerDocument(document, virtualFile); - } - // returns true if shreds were set, false if old ones were reused private static boolean cacheEverything(@NotNull Place place, @NotNull DocumentWindowImpl documentWindow, @NotNull InjectedFileViewProvider viewProvider, @NotNull PsiFile psiFile, - @NotNull SmartPsiElementPointer pointer) { + @NotNull SmartPsiElementPointer pointer, + @NotNull PsiDocumentManagerBase documentManager) { FileDocumentManagerImpl.registerDocument(documentWindow, viewProvider.getVirtualFile()); DebugUtil.startPsiModification("MultiHostRegistrar cacheEverything"); @@ -439,7 +379,7 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost } psiFile.putUserData(FileContextUtil.INJECTED_IN_ELEMENT, pointer); - ((PsiDocumentManagerBase)PsiDocumentManager.getInstance(psiFile.getProject())).associatePsi(documentWindow, psiFile); + documentManager.associatePsi(documentWindow, psiFile); keepTreeFromChameleoningBack(psiFile); @@ -453,14 +393,12 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost @NotNull PsiFile hostPsiFile, @NotNull VirtualFile hostVirtualFile, @NotNull DocumentEx hostDocument, - @NotNull PsiElement contextElement, @NotNull List placeInfos) { return msg + ".\n" + - language + ";\n " + - "Host file: " + hostPsiFile + " in '" + hostVirtualFile.getPresentableUrl() + "'" + + "OK let's see. Host file: " + hostPsiFile + " in '" + hostVirtualFile.getPresentableUrl() + "' (" + hostPsiFile.getLanguage()+") " + (PsiDocumentManager.getInstance(hostPsiFile.getProject()).isUncommited(hostDocument) ? " (uncommitted)" : "") + "\n" + - "Context element " + contextElement.getTextRange() + ": '" + contextElement + "'; " + - "Ranges: " + placeInfos; + "Was injected " + language + + " at ranges: " + placeInfos; } private static final Key TREE_HARD_REF = Key.create("TREE_HARD_REF"); @@ -524,35 +462,6 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost } - private static void patchLeaves(@NotNull ASTNode parsedNode, - @NotNull Place shreds, - @NotNull List escapers) { - LeafPatcher patcher = new LeafPatcher(shreds, escapers); - ((TreeElement)parsedNode).acceptTree(patcher); - - assert ((TreeElement)parsedNode).textMatches(patcher.catLeafs) : "Malformed PSI structure: leaf texts do not add up to the whole file text." + - "\nFile text (from tree) :'"+parsedNode.getText()+"'" + - "\nFile text (from PSI) :'"+parsedNode.getPsi().getText()+"'" + - "\nLeaf texts concatenated:'"+ patcher.catLeafs +"';" + - "\nFile root: "+parsedNode+ - "\nLanguage: "+parsedNode.getPsi().getLanguage()+ - "\nHost file: "+ shreds.getHostPointer().getVirtualFile() - ; - DebugUtil.startPsiModification("injection leaf patching"); - try { - for (Map.Entry entry : patcher.newTexts.entrySet()) { - LeafElement leaf = entry.getKey(); - String newText = entry.getValue(); - leaf.rawReplaceWithText(newText); - } - } - finally { - DebugUtil.finishPsiModification(); - } - - TreeUtil.clearCaches((TreeElement)parsedNode); - } - // under InjectedLanguageManagerImpl.ourInjectionPsiLock @NotNull private static PsiFile registerDocument(@NotNull DocumentWindowImpl newDocumentWindow, @@ -602,7 +511,7 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost return newInjectedPsi; } - private static void mergePsi(@NotNull PsiFileImpl oldFile, + private static void mergePsi(@NotNull PsiFile oldFile, @NotNull ASTNode oldFileNode, @NotNull PsiFile injectedPsi, @NotNull ASTNode injectedNode) { @@ -611,7 +520,7 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost oldViewProvider.performNonPhysically(() -> { DebugUtil.startPsiModification("injected tree diff"); try { - final DiffLog diffLog = BlockSupportImpl.mergeTrees(oldFile, oldFileNode, injectedNode, new DaemonProgressIndicator(), + final DiffLog diffLog = BlockSupportImpl.mergeTrees((PsiFileImpl)oldFile, oldFileNode, injectedNode, new DaemonProgressIndicator(), oldFileNode.getText()); DocumentCommitThread.doActualPsiChange(oldFile, diffLog); } @@ -622,7 +531,226 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost } } - private static boolean intersect(DocumentWindowImpl doc1, DocumentWindowImpl doc2) { + /** + * {@code language} was injected in {@code hostPsiFile} (located in {@code hostVirtualFile}) + * and corresponding injected {@code oldInjectedPsi} (along with {@code oldDocumentWindow} and {@code oldInjectedVirtualFile}) were created. + * Then the user came along and changed the host document in {@code hostVirtualFile}. + * Document commit started and produced PSI diff {@code oldRoot} -> {@code newRoot} in the host PSI. + * + * Now we try to produce similar diff for the injected fragment PSI. + * To do that, we: + *
+   * - calculate where injected shreds from oldRoot will be located in the newRoot
+   *   -- get old range markers for shreds
+   *   -- calculate new ranges by applying doc changes in {@link SelfElementInfo#calcActualRangeAfterDocumentEvents(PsiFile, Document, Segment, boolean)}
+   *      (have to do all that manually because smart pointers are not yet updated)
+   * - find similar PsiLanguageInjectionHost in the {@code newRoot} at these ranges
+   *   (since newRoot is a non-physical copy of hostPsiFile with PSI diff applied, we have to do that semi-manually too)
+   * - create fake injection using this new injection host (along with characters decoding/leaf patching)
+   *   (see call to {@link #parseFile(Language, Language, DocumentWindowImpl, VirtualFile, DocumentEx, PsiFile, Project, CharSequence, List, StringBuilder, String)} )
+   * - feed two injections, the old and the new created fake to the standard tree diff
+   *   (see call to {@link BlockSupportImpl#mergeTrees(PsiFileImpl, ASTNode, ASTNode, ProgressIndicator, CharSequence)} )
+   * - return continuation which performs actual PSI replace, just like {@link DocumentCommitThread#doCommit(DocumentCommitThread.CommitTask, PsiFile, FileASTNode, ProperTextRange, List)} does
+   *   {@code null} means we failed to reparse and will have to kill the injection.
+   * 
+ */ + static BooleanRunnable reparse(@NotNull Language language, + @NotNull DocumentWindowImpl oldDocumentWindow, + @NotNull PsiFile oldInjectedPsi, + @NotNull VirtualFileWindow oldInjectedVirtualFile, + @NotNull VirtualFile hostVirtualFile, + @NotNull PsiFile hostPsiFile, + @NotNull ProgressIndicator indicator, + @NotNull ASTNode oldRoot, @NotNull ASTNode newRoot) { + synchronized (InjectedLanguageManagerImpl.ourInjectionPsiLock) { + Project project = hostPsiFile.getProject(); + String newText = oldDocumentWindow.getText(); + FileASTNode oldNode = oldInjectedPsi.getNode(); + InjectedFileViewProvider oldInjectedPsiViewProvider = (InjectedFileViewProvider)oldInjectedPsi.getViewProvider(); + String oldPsiText = oldNode.getText(); + if (newText.equals(oldPsiText)) return ()->true; + if (oldDocumentWindow.isOneLine() && newText.contains("\n") != oldPsiText.contains("\n")) { + // one-lineness changed, e.g. when enter pressed in the middle of a string literal + return null; + } + Place oldPlace = oldDocumentWindow.getShreds(); + // can be different from newText if decode fails in the middle and we'll have to shrink the document + StringBuilder newDocumentText = new StringBuilder(newText.length()); + // we need escaper but it only works with committed PSI, + // so we get the committed (but not yet applied) PSI from the commit-document-in-the-background process + // and find the corresponding injection host there + // and create literal escaper from that new (dummy) psi + List placeInfos = new SmartList<>(); + StringBuilder chars = new StringBuilder(); + for (PsiLanguageInjectionHost.Shred shred : oldPlace) { + PsiLanguageInjectionHost oldHost = shred.getHost(); + if (oldHost == null) return null; + SmartPsiElementPointer hostPointer = ((ShredImpl)shred).getSmartPointer(); + Segment hostRangeMarker = calcActualRange(hostPointer, oldDocumentWindow.getDelegate()); + if (hostRangeMarker == null) return null; + PsiLanguageInjectionHost newDummyInjectionHost = findNewInjectionHost(hostPsiFile, oldRoot, newRoot, oldHost, hostRangeMarker); + if (newDummyInjectionHost == null) { + return null; + } + Segment hostInjectionRange = shred.getHostRangeMarker(); // in the new document + if (hostInjectionRange == null) return null; + TextRange rangeInsideHost = TextRange.create(hostInjectionRange).shiftLeft(hostRangeMarker.getStartOffset()); + + PlaceInfo info = new PlaceInfo(shred.getPrefix(), shred.getSuffix(), newDummyInjectionHost, rangeInsideHost); + placeInfos.add(info); + info.hostSmartPointer = hostPointer; + + decode(info, chars); + + // pass the old pointers because their offsets will be adjusted automatically (SmartPsiElementPointer does that) + TextRange rangeInHostElementPSI = info.rangeInHostElement; + + newDocumentText.append(shred.getPrefix()); + newDocumentText.append(newDummyInjectionHost.getText(), rangeInHostElementPSI.getStartOffset(), rangeInHostElementPSI.getEndOffset()); + newDocumentText.append(shred.getSuffix()); + } + // newDocumentText can be shorter if decode failed + //assert newText.equals(newDocumentText.toString()) : "-\n"+newText+"\n--\n"+newDocumentText+"\n---\n"; + + PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(project); + DocumentEx hostDocument = oldDocumentWindow.getDelegate(); + assert documentManager.isUncommited(hostDocument); + String fileName = ((VirtualFileWindowImpl)oldInjectedVirtualFile).getName(); + ASTNode parsedNode = parseFile(language, language, oldDocumentWindow, + hostVirtualFile, hostDocument, hostPsiFile, project, newDocumentText, placeInfos, chars, + fileName); + + DiffLog diffLog = BlockSupportImpl.mergeTrees((PsiFileImpl)oldInjectedPsi, oldNode, parsedNode, indicator, oldPsiText); + + return () -> { + oldInjectedPsiViewProvider.performNonPhysically(() -> { + DebugUtil.startPsiModification("injected tree diff"); + try { + DocumentCommitThread.doActualPsiChange(oldInjectedPsi, diffLog); + + PsiDocumentManagerBase documentManagerBase = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(project); + + // create new shreds after commit is complete because otherwise the range markers will be changed in MarkerCache.updateMarkers + Place newPlace = new Place(); + for (int i = 0; i < oldPlace.size(); i++) { + PsiLanguageInjectionHost.Shred shred = oldPlace.get(i); + PlaceInfo info = placeInfos.get(i); + TextRange rangeInDecodedPSI = info.rangeInDecodedPSI; + TextRange rangeInHostElementPSI = info.rangeInHostElement; + ShredImpl newShred = ((ShredImpl)shred).withRange(rangeInDecodedPSI, rangeInHostElementPSI); + newPlace.add(newShred); + } + + SmartPsiElementPointer pointer = placeInfos.get(0).hostSmartPointer; + cacheEverything(newPlace, oldDocumentWindow, oldInjectedPsiViewProvider, oldInjectedPsi, pointer, documentManagerBase); + String docText = oldDocumentWindow.getText(); + assert docText.equals(newText) : "=\n" + docText + "\n==\n" + newDocumentText + "\n===\n"; + } + finally { + DebugUtil.finishPsiModification(); + } + }); + return true; + }; + } + } + + private static Segment calcActualRange(SmartPsiElementPointer pointer, + Document document) { + PsiFile containingFile = pointer.getContainingFile(); + return SelfElementInfo.calcActualRangeAfterDocumentEvents(containingFile, document, pointer.getPsiRange(), true); + } + + @NotNull + private static ASTNode parseFile(@NotNull Language language, Language forcedLanguage, + @NotNull DocumentWindowImpl documentWindow, + @NotNull VirtualFile hostVirtualFile, + @NotNull DocumentEx hostDocument, @NotNull PsiFile hostPsiFile, + @NotNull Project project, + @NotNull CharSequence documentText, + @NotNull List placeInfos, + @NotNull StringBuilder decodedChars, + @NotNull String fileName) { + VirtualFileWindowImpl virtualFile = new VirtualFileWindowImpl(fileName, hostVirtualFile, documentWindow, language, decodedChars); + Language finalLanguage = forcedLanguage == null ? LanguageSubstitutors.INSTANCE.substituteLanguage(language, virtualFile, project) : forcedLanguage; + InjectedFileViewProvider viewProvider = new InjectedFileViewProvider(PsiManager.getInstance(project), virtualFile, documentWindow, finalLanguage); + ParserDefinition parserDefinition = LanguageParserDefinitions.INSTANCE.forLanguage(finalLanguage); + assert parserDefinition != null : "Parser definition for language " + finalLanguage + " is null"; + PsiFile psiFile = parserDefinition.createFile(viewProvider); + + final ASTNode parsedNode = keepTreeFromChameleoningBack(psiFile); + + assert parsedNode instanceof FileElement : "Parsed to " + parsedNode + " instead of FileElement"; + + assert ((FileElement)parsedNode).textMatches(decodedChars) : exceptionContext("Before patch: doc:\n'" + documentText + "'\n---PSI:\n'" + parsedNode.getText() + "'\n---chars:\n'" + + decodedChars + "'", + finalLanguage, hostPsiFile, hostVirtualFile, + hostDocument, placeInfos); + try { + patchLeaves(placeInfos, viewProvider, parsedNode, documentText); + } + catch (PatchException e) { + throw new RuntimeException(exceptionContext(e.getMessage()+"'\n---chars:\n'" + decodedChars + "'", finalLanguage, hostPsiFile, hostVirtualFile, hostDocument, placeInfos)); + } + + virtualFile.setContent(null, decodedChars, false); + virtualFile.setWritable(virtualFile.getDelegate().isWritable()); + + + try { + List, TextRange>> + tokens = obtainHighlightTokensFromLexer(language, decodedChars, virtualFile, project, placeInfos); + psiFile.putUserData(InjectedLanguageUtil.HIGHLIGHT_TOKENS, tokens); + } + catch (ProcessCanceledException e) { + throw e; + } + catch (RuntimeException e) { + throw new RuntimeException(exceptionContext("Obtaining tokens error", language, hostPsiFile, hostVirtualFile, hostDocument, placeInfos), e); + } + + return parsedNode; + } + + // find PsiLanguageInjectionHost in range "newInjectionHostRange" in the file which is almost "hostPsiFile" but where "oldRoot" replaced with "newRoot" + private static PsiLanguageInjectionHost findNewInjectionHost(@NotNull PsiFile hostPsiFile, + @NotNull ASTNode oldRoot, + @NotNull ASTNode newRoot, + @NotNull PsiLanguageInjectionHost oldInjectionHost, + @NotNull Segment newInjectionHostRange) { + TextRange oldRootRange = oldRoot.getTextRange(); + TextRange newRootRange = newRoot.getTextRange(); + PsiElement toLookIn; + int startToLook; + int endToLook; + if (newInjectionHostRange.getEndOffset() <= oldRootRange.getStartOffset()) { + // find left of the change + toLookIn = hostPsiFile; + startToLook = newInjectionHostRange.getStartOffset(); + endToLook = newInjectionHostRange.getEndOffset(); + } + else if (newInjectionHostRange.getStartOffset() >= oldRootRange.getStartOffset() + newRootRange.getLength()) { + // find right of the change + toLookIn = hostPsiFile; + startToLook = newInjectionHostRange.getStartOffset() + newRootRange.getLength() - oldRootRange.getLength(); + endToLook = newInjectionHostRange.getEndOffset() + newRootRange.getLength() - oldRootRange.getLength(); + } + else { + // inside + toLookIn = newRoot.getPsi(); + if (toLookIn instanceof PsiFile) { + FileViewProvider viewProvider = ((PsiFile)toLookIn).getViewProvider(); + toLookIn = ObjectUtils.notNull(viewProvider.getPsi(hostPsiFile.getLanguage()), viewProvider.getPsi(viewProvider.getBaseLanguage())); + } + startToLook = newInjectionHostRange.getStartOffset() - oldRootRange.getStartOffset(); + endToLook = newInjectionHostRange.getEndOffset() - oldRootRange.getStartOffset(); + } + Identikit.ByType kit = Identikit.fromPsi(oldInjectionHost, hostPsiFile.getLanguage()); + return (PsiLanguageInjectionHost)kit.findInside(toLookIn, startToLook, endToLook); + } + + + static boolean intersect(DocumentWindowImpl doc1, DocumentWindowImpl doc2) { Segment[] hostRanges1 = doc1.getHostRanges(); Segment[] hostRanges2 = doc2.getHostRanges(); // DocumentWindowImpl.getHostRanges() may theoretically return non-sorted ranges @@ -641,10 +769,9 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost private static List, TextRange>> obtainHighlightTokensFromLexer(@NotNull Language language, @NotNull CharSequence outChars, - @NotNull Place shreds, @NotNull VirtualFileWindow virtualFile, @NotNull Project project, - @NotNull List escapers) { + @NotNull List placeInfos) { SyntaxHighlighter syntaxHighlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(language, project, (VirtualFile)virtualFile); Lexer lexer = syntaxHighlighter.getHighlightingLexer(); lexer.start(outChars); @@ -662,14 +789,14 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost while (range != null && !range.isEmpty()) { if (range.getStartOffset() >= shredEndOffset) { hostNum++; - PsiLanguageInjectionHost.Shred shred = shreds.get(hostNum); - shredEndOffset = shred.getRange().getEndOffset(); + PlaceInfo info = placeInfos.get(hostNum); + shredEndOffset = info.rangeInDecodedPSI.getEndOffset(); prevHostEndOffset = range.getStartOffset(); - hostPtr = ((ShredImpl)shred).getSmartPointer(); - escaper = escapers.get(hostNum); - rangeInsideHost = shred.getRangeInsideHost(); - prefixLength = shred.getPrefix().length(); - suffixLength = shred.getSuffix().length(); + hostPtr = info.hostSmartPointer; + escaper = info.myEscaper; + rangeInsideHost = info.rangeInHostElement; + prefixLength = info.prefix.length(); + suffixLength = info.suffix.length(); } //in prefix/suffix or spills over to next fragment if (range.getStartOffset() < prevHostEndOffset + prefixLength) { diff --git a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/LeafPatcher.java b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/LeafPatcher.java index 171d4b1d1c54..32700bd5d884 100644 --- a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/LeafPatcher.java +++ b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/LeafPatcher.java @@ -16,15 +16,14 @@ package com.intellij.psi.impl.source.tree.injected; +import com.intellij.lang.ASTNode; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.TextRange; import com.intellij.psi.LiteralTextEscaper; import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiLanguageInjectionHost; -import com.intellij.psi.impl.source.tree.ForeignLeafPsiElement; -import com.intellij.psi.impl.source.tree.LeafElement; -import com.intellij.psi.impl.source.tree.RecursiveTreeElementWalkingVisitor; +import com.intellij.psi.impl.DebugUtil; +import com.intellij.psi.impl.source.tree.*; import gnu.trove.THashMap; import org.jetbrains.annotations.NotNull; @@ -39,14 +38,15 @@ class LeafPatcher extends RecursiveTreeElementWalkingVisitor { private String hostText; private LiteralTextEscaper currentTextEscaper; private TextRange rangeInHost; - private final Place myShreds; - @NotNull private final List myEscapers; - final Map newTexts = new THashMap<>(); - final StringBuilder catLeafs = new StringBuilder(); + private final Map newTexts = new THashMap<>(); + @NotNull + private final List myPlaceInfos; + private final StringBuilder catLeafs; + private final StringBuilder tempLeafBuffer = new StringBuilder(); - LeafPatcher(@NotNull Place shreds, @NotNull List escapers) { - myShreds = shreds; - myEscapers = escapers; + LeafPatcher(@NotNull List placeInfos, int approxTextLength) { + myPlaceInfos = placeInfos; + catLeafs = new StringBuilder(approxTextLength); } @Override @@ -65,25 +65,24 @@ class LeafPatcher extends RecursiveTreeElementWalkingVisitor { private StringBuilder constructTextFromHostPSI(int startOffset, int endOffset) { boolean firstTimer = false; - PsiLanguageInjectionHost.Shred current = myShreds.get(shredNo); + PlaceInfo currentPlace = myPlaceInfos.get(shredNo); if (hostText == null) { - PsiLanguageInjectionHost host = current.getHost(); - hostText = host.getText(); - rangeInHost = current.getRangeInsideHost(); - currentTextEscaper = myEscapers.get(shredNo); + hostText = currentPlace.myHostText; + rangeInHost = currentPlace.getRelevantRangeInsideHost(); + currentTextEscaper = currentPlace.myEscaper; firstTimer = true; } - StringBuilder text = new StringBuilder(endOffset-startOffset); + StringBuilder text = tempLeafBuffer; + text.setLength(0); while (startOffset < endOffset) { - TextRange shredRange = current.getRange(); - String prefix = current.getPrefix(); + TextRange shredRange = currentPlace.rangeInDecodedPSI; + String prefix = currentPlace.prefix; if (startOffset >= shredRange.getEndOffset()) { - current = myShreds.get(++shredNo); - PsiLanguageInjectionHost host = current.getHost(); - hostText = host.getText(); - currentTextEscaper = myEscapers.get(shredNo); - rangeInHost = current.getRangeInsideHost(); + currentPlace = myPlaceInfos.get(++shredNo); + hostText = currentPlace.myHostText; + currentTextEscaper = currentPlace.myEscaper; + rangeInHost = currentPlace.getRelevantRangeInsideHost(); firstTimer = true; continue; } @@ -96,21 +95,19 @@ class LeafPatcher extends RecursiveTreeElementWalkingVisitor { continue; } - String suffix = current.getSuffix(); + String suffix = currentPlace.suffix; if (startOffset < shredRange.getEndOffset() - suffix.length()) { // inside host body, cut out from the host text - int startOffsetInHost = currentTextEscaper.getOffsetInHost( - startOffset - shredRange.getStartOffset() - prefix.length(), rangeInHost); + int startOffsetInHost = currentTextEscaper.getOffsetInHost(startOffset - shredRange.getStartOffset() - prefix.length(), rangeInHost); int endOffsetCut = Math.min(endOffset, shredRange.getEndOffset() - suffix.length()); - int endOffsetInHost = currentTextEscaper.getOffsetInHost( - endOffsetCut - shredRange.getStartOffset() - prefix.length(), rangeInHost); + int endOffsetInHost = currentTextEscaper.getOffsetInHost(endOffsetCut - shredRange.getStartOffset() - prefix.length(), rangeInHost); if (endOffsetInHost != -1) { - if (firstTimer ) text.append(hostText, rangeInHost.getStartOffset(), startOffsetInHost); + if (firstTimer) text.append(hostText, rangeInHost.getStartOffset(), startOffsetInHost); text.append(hostText, startOffsetInHost, endOffsetInHost); startOffset = endOffsetCut; // todo what about lastTimer? - continue; } + continue; } // inside suffix @@ -123,10 +120,36 @@ class LeafPatcher extends RecursiveTreeElementWalkingVisitor { } static final Key UNESCAPED_TEXT = Key.create("INJECTED_UNESCAPED_TEXT"); - private static void storeUnescapedTextFor(final LeafElement leaf, final String leafText) { + private static void storeUnescapedTextFor(@NotNull LeafElement leaf, @NotNull String leafText) { PsiElement psi = leaf.getPsi(); if (psi != null) { psi.putCopyableUserData(UNESCAPED_TEXT, leafText); } } + + void patch(@NotNull ASTNode parsedNode, @NotNull List placeInfos) { + ((TreeElement)parsedNode).acceptTree(this); + + assert ((TreeElement)parsedNode).textMatches(catLeafs) : + "Malformed PSI structure: leaf texts do not add up to the whole file text." + + "\nFile text (from tree) :'" + parsedNode.getText() + "'" + + "\nFile text (from PSI) :'" + parsedNode.getPsi().getText() + "'" + + "\nLeaf texts concatenated:'" + catLeafs + "';" + + "\nFile root: " + parsedNode + + "\nLanguage: " + parsedNode.getPsi().getLanguage() + + "\nHost file: " + placeInfos.get(0).host.getContainingFile().getVirtualFile(); + DebugUtil.startPsiModification("injection leaf patching"); + try { + for (Map.Entry entry : newTexts.entrySet()) { + LeafElement leaf = entry.getKey(); + String newText = entry.getValue(); + leaf.rawReplaceWithText(newText); + } + } + finally { + DebugUtil.finishPsiModification(); + } + + TreeUtil.clearCaches((TreeElement)parsedNode); + } } diff --git a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/PlaceInfo.java b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/PlaceInfo.java new file mode 100644 index 000000000000..9f9b1aaf7e93 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/PlaceInfo.java @@ -0,0 +1,54 @@ +/* + * Copyright 2000-2018 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. + */ +package com.intellij.psi.impl.source.tree.injected; + +import com.intellij.lang.injection.MultiHostRegistrar; +import com.intellij.openapi.util.ProperTextRange; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.LiteralTextEscaper; +import com.intellij.psi.PsiLanguageInjectionHost; +import com.intellij.psi.SmartPsiElementPointer; +import org.jetbrains.annotations.NotNull; + +/** + * stores injection registration info temporarily + * from the {@link MultiHostRegistrar#startInjecting(com.intellij.lang.Language)} call + * to the moment of {@link MultiHostRegistrar#doneInjecting()} + */ +class PlaceInfo { + @NotNull final String prefix; + @NotNull final String suffix; + @NotNull final PsiLanguageInjectionHost host; + @NotNull final TextRange registeredRangeInsideHost; + @NotNull final LiteralTextEscaper myEscaper; + @NotNull final String myHostText; + ProperTextRange rangeInDecodedPSI; + TextRange rangeInHostElement; + SmartPsiElementPointer hostSmartPointer; + + PlaceInfo(@NotNull String prefix, + @NotNull String suffix, + @NotNull PsiLanguageInjectionHost host, + @NotNull TextRange registeredRangeInsideHost) { + this.prefix = prefix; + this.suffix = suffix; + this.host = host; + this.registeredRangeInsideHost = registeredRangeInsideHost; + myEscaper = host.createLiteralTextEscaper(); + myHostText = host.getText(); + } + + @Override + public String toString() { + return "Shred " + + (prefix.isEmpty() ? "" : "prefix='"+prefix+"' ") + + (suffix.isEmpty() ? "" : "suffix='"+suffix+"' ") + + "in " + host + " with range " + host.getTextRange()+" "+ + "inside range " + registeredRangeInsideHost; + } + + TextRange getRelevantRangeInsideHost() { + return myEscaper.getRelevantTextRange().intersection(registeredRangeInsideHost); + } +} diff --git a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/ShredImpl.java b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/ShredImpl.java index 35bc824b5ed2..7fd8de91f4d6 100644 --- a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/ShredImpl.java +++ b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/ShredImpl.java @@ -19,13 +19,14 @@ import com.intellij.openapi.util.ProperTextRange; import com.intellij.openapi.util.Segment; import com.intellij.openapi.util.TextRange; import com.intellij.psi.*; +import com.intellij.psi.impl.smartPointers.SmartPointerManagerImpl; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; class ShredImpl implements PsiLanguageInjectionHost.Shred { private final SmartPsiFileRange relevantRangeInHost; private final SmartPsiElementPointer hostElementPointer; - private final TextRange range; // range in (decoded) PSI + private final TextRange rangeInDecodedPSI; // range in (decoded) PSI private final String prefix; private final String suffix; private final boolean usePsiRange; @@ -35,13 +36,13 @@ class ShredImpl implements PsiLanguageInjectionHost.Shred { @NotNull SmartPsiElementPointer hostElementPointer, @NotNull String prefix, @NotNull String suffix, - @NotNull TextRange range, + @NotNull TextRange rangeInDecodedPSI, boolean usePsiRange, boolean isOneLine) { this.hostElementPointer = hostElementPointer; this.relevantRangeInHost = relevantRangeInHost; this.prefix = prefix; this.suffix = suffix; - this.range = range; + this.rangeInDecodedPSI = rangeInDecodedPSI; this.usePsiRange = usePsiRange; this.isOneLine = isOneLine; @@ -56,8 +57,20 @@ class ShredImpl implements PsiLanguageInjectionHost.Shred { assert host != null && host.isValid() : "no host: " + hostElementPointer; } + @NotNull ShredImpl withPsiRange() { - return new ShredImpl(relevantRangeInHost, hostElementPointer, prefix, suffix, range, true, isOneLine); + return new ShredImpl(relevantRangeInHost, hostElementPointer, prefix, suffix, rangeInDecodedPSI, true, isOneLine); + } + @NotNull + ShredImpl withRange(@NotNull TextRange rangeInDecodedPSI, @NotNull TextRange rangeInHostElementPSI) { + SmartPsiFileRange rangeMarker = relevantRangeInHost; + Segment oldRangeInHostElementPSI = calcRangeInsideHostElement(false); + if (!rangeInHostElementPSI.equals(TextRange.create(oldRangeInHostElementPSI))) { + SmartPointerManager pointerManager = SmartPointerManager.getInstance(rangeMarker.getProject()); + Segment hostElementRange = hostElementPointer.getRange(); + rangeMarker = ((SmartPointerManagerImpl)pointerManager).createSmartPsiFileRangePointer(rangeMarker.getContainingFile(), rangeInHostElementPSI.shiftRight(hostElementRange.getStartOffset()), true); + } + return new ShredImpl(rangeMarker, hostElementPointer, prefix, suffix, rangeInDecodedPSI, usePsiRange, isOneLine); } @NotNull @@ -74,19 +87,25 @@ class ShredImpl implements PsiLanguageInjectionHost.Shred { @Override @NotNull public TextRange getRangeInsideHost() { + return calcRangeInsideHostElement(true); + } + + @NotNull + private TextRange calcRangeInsideHostElement(boolean usePsiRange) { PsiLanguageInjectionHost host = getHost(); - Segment psiRange = relevantRangeInHost.getPsiRange(); + Segment psiRange = usePsiRange ? relevantRangeInHost.getPsiRange() : relevantRangeInHost.getRange(); TextRange textRange = psiRange == null ? null : TextRange.create(psiRange); if (host == null) { if (textRange != null) return textRange; - Segment fromSP = hostElementPointer.getPsiRange(); + Segment fromSP = usePsiRange ? hostElementPointer.getPsiRange() : hostElementPointer.getRange(); if (fromSP != null) return TextRange.create(fromSP); return new TextRange(0,0); } TextRange hostTextRange = host.getTextRange(); textRange = textRange == null ? null : textRange.intersection(hostTextRange); if (textRange == null) return new ProperTextRange(0, hostTextRange.getLength()); - return textRange.shiftRight(-hostTextRange.getStartOffset()); + + return textRange.shiftLeft(hostTextRange.getStartOffset()); } @Override @@ -96,8 +115,7 @@ class ShredImpl implements PsiLanguageInjectionHost.Shred { Segment hostRange = getHostRangeMarker(); return "Shred " + (host == null ? null : host.getTextRange()) + ": " + host + " In host range: " + (hostRange != null ? "(" + hostRange.getStartOffset() + "," + hostRange.getEndOffset() + ");" : "invalid;") + - " PSI range: " + - range; + " PSI range: " + rangeInDecodedPSI; } @Override @@ -119,15 +137,15 @@ class ShredImpl implements PsiLanguageInjectionHost.Shred { host.equals(shred.getHost()) && prefix.equals(shred.getPrefix()) && suffix.equals(shred.getSuffix()) && - range.equals(shred.getRange()) && + rangeInDecodedPSI.equals(shred.getRange()) && hostRangeMarker != null && hostRangeMarker2 != null && - TextRange.create(hostRangeMarker).equals(TextRange.create(hostRangeMarker2)); + TextRange.areSegmentsEqual(hostRangeMarker, hostRangeMarker2); } @Override public int hashCode() { - return range.hashCode(); + return rangeInDecodedPSI.hashCode(); } @Override @@ -143,7 +161,7 @@ class ShredImpl implements PsiLanguageInjectionHost.Shred { @NotNull @Override public TextRange getRange() { - return range; + return rangeInDecodedPSI; } @NotNull From 413ecc75a59a45ca4756a572bc519d908c34511e Mon Sep 17 00:00:00 2001 From: Alexey Kudravtsev Date: Thu, 11 Jan 2018 16:40:23 +0300 Subject: [PATCH 7/7] IDEA-CR-28139 cleanup --- .../execution/impl/ConsoleViewImplTest.java | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/platform/platform-tests/testSrc/com/intellij/execution/impl/ConsoleViewImplTest.java b/platform/platform-tests/testSrc/com/intellij/execution/impl/ConsoleViewImplTest.java index 381e60e74eed..e57a09348f72 100644 --- a/platform/platform-tests/testSrc/com/intellij/execution/impl/ConsoleViewImplTest.java +++ b/platform/platform-tests/testSrc/com/intellij/execution/impl/ConsoleViewImplTest.java @@ -415,31 +415,25 @@ public class ConsoleViewImplTest extends LightPlatformTestCase { public void testInputFilter() { Disposer.dispose(myConsole); // have to re-init extensions List> registered = new ArrayList<>(); - ExtensionPoint extensionPoint = Extensions.getRootArea().getExtensionPoint(ConsoleInputFilterProvider.INPUT_FILTER_PROVIDERS); ConsoleInputFilterProvider crazyProvider = project -> new InputFilter[]{ (text, contentType) -> { registered.add(Pair.create(text, contentType)); return Collections.singletonList(Pair.create("+!" + text + "-!", contentType)); } }; - extensionPoint.registerExtension(crazyProvider); + PlatformTestUtil.registerExtension(ConsoleInputFilterProvider.INPUT_FILTER_PROVIDERS, crazyProvider, getTestRootDisposable()); myConsole = createConsole(); - try { - StringBuilder expectedText = new StringBuilder(); - List> expectedRegisteredTokens = new ArrayList<>(); - for (int i=0;i<25;i++) { - String chunk = i + ""; - myConsole.print(chunk, ConsoleViewContentType.USER_INPUT); - expectedText.append("+!" + i + "-!"); - expectedRegisteredTokens.add(Pair.create(chunk, ConsoleViewContentType.USER_INPUT)); - } - myConsole.flushDeferredText(); - myConsole.waitAllRequests(); - assertEquals(expectedText.toString(), myConsole.getText()); - assertEquals(expectedRegisteredTokens, registered); - } - finally { - extensionPoint.unregisterExtension(crazyProvider); + StringBuilder expectedText = new StringBuilder(); + List> expectedRegisteredTokens = new ArrayList<>(); + for (int i=0;i<25;i++) { + String chunk = i + ""; + myConsole.print(chunk, ConsoleViewContentType.USER_INPUT); + expectedText.append("+!" + i + "-!"); + expectedRegisteredTokens.add(Pair.create(chunk, ConsoleViewContentType.USER_INPUT)); } + myConsole.flushDeferredText(); + myConsole.waitAllRequests(); + assertEquals(expectedText.toString(), myConsole.getText()); + assertEquals(expectedRegisteredTokens, registered); } }