From 521cea8535afbbdcc892718ada5588104f9ba94c Mon Sep 17 00:00:00 2001 From: Alexey Kudravtsev Date: Fri, 1 Dec 2017 16:19:55 +0300 Subject: [PATCH] refactored injections to avoid caching non-thread-safe InjectionRegistrar --- .../JavaConcatenationInjectorManager.java | 100 ++-- .../injected/InjectedFileViewProvider.java | 37 +- .../injected/InjectedLanguageManagerImpl.java | 5 +- .../tree/injected/InjectedLanguageUtil.java | 99 ++-- .../InjectedPsiCachedValueProvider.java | 21 +- .../tree/injected/InjectionRegistrarImpl.java | 468 +++++++++++------- .../source/tree/injected/InjectionResult.java | 61 +++ .../source/tree/injected/LeafPatcher.java | 18 +- .../psi/impl/source/tree/injected/Place.java | 5 +- .../impl/source/tree/injected/ShredImpl.java | 10 +- 10 files changed, 492 insertions(+), 332 deletions(-) create mode 100644 platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectionResult.java diff --git a/java/java-impl/src/com/intellij/psi/impl/source/tree/injected/JavaConcatenationInjectorManager.java b/java/java-impl/src/com/intellij/psi/impl/source/tree/injected/JavaConcatenationInjectorManager.java index 525dff5a47ad..3b528f6773fe 100644 --- a/java/java-impl/src/com/intellij/psi/impl/source/tree/injected/JavaConcatenationInjectorManager.java +++ b/java/java-impl/src/com/intellij/psi/impl/source/tree/injected/JavaConcatenationInjectorManager.java @@ -62,55 +62,23 @@ public class JavaConcatenationInjectorManager extends SimpleModificationTracker return ServiceManager.getService(project, JavaConcatenationInjectorManager.class); } - private static Pair computeAnchorAndOperandsImpl(@NotNull PsiElement context) { - PsiElement element = context; - PsiElement parent = context.getParent(); - while (parent instanceof PsiPolyadicExpression && ((PsiPolyadicExpression)parent).getOperationTokenType() == JavaTokenType.PLUS - || parent instanceof PsiAssignmentExpression && ((PsiAssignmentExpression)parent).getOperationTokenType() == JavaTokenType.PLUSEQ - || parent instanceof PsiConditionalExpression && ((PsiConditionalExpression)parent).getCondition() != element - || parent instanceof PsiTypeCastExpression - || parent instanceof PsiParenthesizedExpression) { - element = parent; - parent = parent.getParent(); - } - - PsiElement[] operands; - PsiElement anchor; - if (element instanceof PsiPolyadicExpression) { - operands = ((PsiPolyadicExpression)element).getOperands(); - anchor = element; - } - else if (element instanceof PsiAssignmentExpression) { - PsiExpression rExpression = ((PsiAssignmentExpression)element).getRExpression(); - operands = new PsiElement[]{rExpression == null ? element : rExpression}; - anchor = element; - } - else { - operands = new PsiElement[]{context}; - anchor = context; - } - - return Pair.create(anchor, operands); - } - - private static InjectionRegistrarImpl doCompute(@NotNull PsiFile containingFile, - @NotNull Project project, - @NotNull PsiElement anchor, - @NotNull PsiElement[] operands) { + private static InjectionResult doCompute(@NotNull PsiFile containingFile, + @NotNull Project project, + @NotNull PsiElement anchor, + @NotNull PsiElement[] operands) { InjectionRegistrarImpl registrar = new InjectionRegistrarImpl(project, containingFile, anchor); + InjectionResult result = null; JavaConcatenationInjectorManager concatenationInjectorManager = getInstance(project); for (ConcatenationAwareInjector concatenationInjector : concatenationInjectorManager.myConcatenationInjectors) { concatenationInjector.getLanguagesToInject(registrar, operands); - if (registrar.getResult() != null) break; + result = registrar.getInjectedResult(); + if (result != null) break; } - if (registrar.getResult() == null) { - registrar = null; - } - return registrar; + return result; } - private static final Key> INJECTED_PSI_IN_CONCATENATION = Key.create("INJECTED_PSI_IN_CONCATENATION"); + private static final Key> INJECTED_PSI_IN_CONCATENATION = Key.create("INJECTED_PSI_IN_CONCATENATION"); private static final Key NO_CONCAT_INJECTION_TIMESTAMP = Key.create("NO_CONCAT_INJECTION_TIMESTAMP"); public abstract static class BaseConcatenation2InjectorAdapter implements MultiHostInjector { @@ -132,8 +100,8 @@ public class JavaConcatenationInjectorManager extends SimpleModificationTracker PsiElement[] operands = pair.second; Integer noInjectionTimestamp = anchor.getUserData(NO_CONCAT_INJECTION_TIMESTAMP); - InjectionRegistrarImpl result; - ParameterizedCachedValue data = null; + InjectionResult result; + ParameterizedCachedValue data = null; if (operands.length == 0 || noInjectionTimestamp != null && noInjectionTimestamp == modificationCount) { result = null; } @@ -145,26 +113,21 @@ public class JavaConcatenationInjectorManager extends SimpleModificationTracker result = doCompute(containingFile, project, anchor, operands); } } - if (result != null && result.getResult() != null) { - for (Pair p : result.getResult()) { - Place place = p.getFirst(); - if (place.isValid()) { - ((InjectionRegistrarImpl)registrar).addToResults(place, p.second, result); - } - } + if (result != null) { + ((InjectionRegistrarImpl)registrar).addToResults(result); if (data == null) { - CachedValueProvider.Result cachedResult = + CachedValueProvider.Result cachedResult = CachedValueProvider.Result.create(result, PsiModificationTracker.MODIFICATION_COUNT, myManager); data = CachedValuesManager.getManager(project).createParameterizedCachedValue( context1 -> { PsiFile containingFile1 = context1.getContainingFile(); Project project1 = containingFile1.getProject(); Pair pair1 = computeAnchorAndOperands(context1); - InjectionRegistrarImpl registrar1 = pair1.second.length == 0 ? null : doCompute(containingFile1, project1, pair1.first, pair1.second); - return registrar1 == null ? null : CachedValueProvider.Result.create(registrar1, PsiModificationTracker.MODIFICATION_COUNT, myManager); + InjectionResult result1 = pair1.second.length == 0 ? null : doCompute(containingFile1, project1, pair1.first, pair1.second); + return result1 == null ? null : CachedValueProvider.Result.create(result1, PsiModificationTracker.MODIFICATION_COUNT, myManager); }, false); - ((PsiParameterizedCachedValue)data).setValue(cachedResult); + ((PsiParameterizedCachedValue)data).setValue(cachedResult); anchor.putUserData(INJECTED_PSI_IN_CONCATENATION, data); if (anchor.getUserData(NO_CONCAT_INJECTION_TIMESTAMP) != null) { @@ -191,7 +154,34 @@ public class JavaConcatenationInjectorManager extends SimpleModificationTracker @Override public Pair computeAnchorAndOperands(@NotNull PsiElement context) { - return computeAnchorAndOperandsImpl(context); + PsiElement element = context; + PsiElement parent = context.getParent(); + while (parent instanceof PsiPolyadicExpression && ((PsiPolyadicExpression)parent).getOperationTokenType() == JavaTokenType.PLUS + || parent instanceof PsiAssignmentExpression && ((PsiAssignmentExpression)parent).getOperationTokenType() == JavaTokenType.PLUSEQ + || parent instanceof PsiConditionalExpression && ((PsiConditionalExpression)parent).getCondition() != element + || parent instanceof PsiTypeCastExpression + || parent instanceof PsiParenthesizedExpression) { + element = parent; + parent = parent.getParent(); + } + + PsiElement[] operands; + PsiElement anchor; + if (element instanceof PsiPolyadicExpression) { + operands = ((PsiPolyadicExpression)element).getOperands(); + anchor = element; + } + else if (element instanceof PsiAssignmentExpression) { + PsiExpression rExpression = ((PsiAssignmentExpression)element).getRExpression(); + operands = new PsiElement[]{rExpression == null ? element : rExpression}; + anchor = element; + } + else { + operands = new PsiElement[]{context}; + anchor = context; + } + + return Pair.create(anchor, operands); } @Override diff --git a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectedFileViewProvider.java b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectedFileViewProvider.java index 2441849839ea..1fca813102ae 100644 --- a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectedFileViewProvider.java +++ b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectedFileViewProvider.java @@ -35,11 +35,7 @@ import org.jetbrains.annotations.NotNull; import java.util.List; -/** - * @author cdr -*/ public class InjectedFileViewProvider extends SingleRootFileViewProvider implements FreeThreadedFileViewProvider { - private Project myProject; private final Object myLock = new Object(); private final DocumentWindowImpl myDocumentWindow; private static final ThreadLocal disabledTemporarily = ThreadLocal.withInitial(() -> false); @@ -51,7 +47,6 @@ public class InjectedFileViewProvider extends SingleRootFileViewProvider impleme @NotNull Language language) { super(psiManager, (VirtualFile)virtualFile, true, language); myDocumentWindow = documentWindow; - myProject = documentWindow.getShreds().getHostPointer().getProject(); } @Override @@ -101,7 +96,7 @@ public class InjectedFileViewProvider extends SingleRootFileViewProvider impleme for (PsiElement current = hostElementCopy; current != null && current != hostPsiFileCopy; current = current.getParent()) { current.putUserData(LANGUAGE_FOR_INJECTED_COPY_KEY, language); try { - InjectedLanguageUtil.enumerate(current, hostPsiFileCopy, false, visitor); + InjectedLanguageManager.getInstance(hostPsiFileCopy.getProject()).enumerateEx(current, hostPsiFileCopy, false, visitor); } finally { current.putUserData(LANGUAGE_FOR_INJECTED_COPY_KEY, null); @@ -113,21 +108,23 @@ public class InjectedFileViewProvider extends SingleRootFileViewProvider impleme static Key LANGUAGE_FOR_INJECTED_COPY_KEY = Key.create("LANGUAGE_FOR_INJECTED_COPY_KEY"); // returns true if shreds were set, false if old ones were reused - boolean setShreds(@NotNull Place newShreds, @NotNull Project project) { + boolean setShreds(@NotNull Place newShreds) { synchronized (myLock) { - myProject = project; Place oldShreds = myDocumentWindow.getShreds(); // try to reuse shreds, otherwise there are too many range markers disposals/re-creations if (same(oldShreds, newShreds)) { return false; } - else { - myDocumentWindow.setShreds(newShreds); - return true; - } + myDocumentWindow.setShreds(newShreds); + return true; } } + @NotNull + private Project getProject() { + return getManager().getProject(); + } + private static boolean same(Place oldShreds, Place newShreds) { if (oldShreds == newShreds) return true; if (oldShreds.size() != newShreds.size()) return false; @@ -144,9 +141,7 @@ public class InjectedFileViewProvider extends SingleRootFileViewProvider impleme } boolean isDisposed() { - synchronized (myLock) { - return myProject.isDisposed(); - } + return getProject().isDisposed(); } Place getShreds() { @@ -161,7 +156,6 @@ public class InjectedFileViewProvider extends SingleRootFileViewProvider impleme @Override public boolean isEventSystemEnabled() { - if (myLock == null) return true; // hack to avoid NPE when this method called from super class constructor return !disabledTemporarily.get(); } @@ -188,8 +182,13 @@ public class InjectedFileViewProvider extends SingleRootFileViewProvider impleme return "Injected file '"+getVirtualFile().getName()+"' " + (isValid() ? "" : " invalid") + (isPhysical() ? "" : " nonphysical"); } - void setPatchingLeaves(boolean patchingLeaves) { - myPatchingLeaves = patchingLeaves; + void doNotInterruptMeWhileImPatchingLeaves(@NotNull Runnable runnable) { + myPatchingLeaves = true; + try { + runnable.run(); + } + finally { + myPatchingLeaves = false; + } } - } 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 d81604ccda6a..a76460ac2e56 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 @@ -418,8 +418,9 @@ public class InjectedLanguageManagerImpl extends InjectedLanguageManager impleme Place shreds = ((DocumentWindowImpl)document).getShreds(); Project project = shreds.getHostPointer().getProject(); DocumentEx delegate = ((PsiDocumentManagerBase)PsiDocumentManager.getInstance(project)).getLastCommittedDocument(document.getDelegate()); - Place place = new Place(ContainerUtil.map(shreds, shred -> ((ShredImpl) shred).withPsiRange())); - return new DocumentWindowImpl(delegate, ((DocumentWindowImpl)document).isOneLine(), place); + Place place = new Place(); + place.addAll(ContainerUtil.map(shreds, shred -> ((ShredImpl)shred).withPsiRange())); + return new DocumentWindowImpl(delegate, document.isOneLine(), 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 7adc7e828f15..584d2aa0645e 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 @@ -39,6 +39,7 @@ import com.intellij.psi.impl.DebugUtil; import com.intellij.psi.impl.PsiManagerEx; import com.intellij.psi.impl.PsiParameterizedCachedValue; import com.intellij.psi.impl.source.DummyHolder; +import com.intellij.psi.injection.ReferenceInjector; import com.intellij.psi.tree.IElementType; import com.intellij.psi.util.*; import com.intellij.testFramework.LightVirtualFile; @@ -151,25 +152,10 @@ public class InjectedLanguageUtil { containingFile = host.getContainingFile(); } - InjectionRegistrarImpl registrar = probeElementsUp(host, containingFile, probeUp); - if (registrar == null) { - // no injections found - return true; - } - List> places = registrar.getResult(); - for (Pair pair : places) { - if (visitor instanceof InjectedReferenceVisitor) { - if (registrar.getReferenceInjector() != null) { - ((InjectedReferenceVisitor)visitor).visitInjectedReference(registrar.getReferenceInjector(), pair.first); - } - } - else if (pair.second != null) { - visitor.visit(pair.second, pair.first); - } - } + probeElementsUp(host, containingFile, probeUp, visitor); return true; } - + /** * Invocation of this method on uncommitted {@code file} can lead to unexpected results, including throwing an exception! */ @@ -293,7 +279,7 @@ public class InjectedLanguageUtil { */ @Nullable public static PsiFile findInjectedPsiNoCommit(@NotNull PsiFile host, int offset) { - PsiElement injected = findInjectedElementNoCommit(host, offset); + PsiElement injected = InjectedLanguageManager.getInstance(host.getProject()).findInjectedElementAt(host, offset); return injected == null ? null : injected.getContainingFile(); } @@ -320,40 +306,57 @@ public class InjectedLanguageUtil { } private static final InjectedPsiCachedValueProvider INJECTED_PSI_PROVIDER = new InjectedPsiCachedValueProvider(); - private static final Key> INJECTED_PSI = Key.create("INJECTED_PSI"); + // list of injected fragments injected into this psi element (can be several if some crazy injector calls startInjecting()/doneInjecting()/startInjecting()/doneInjecting()) + private static final Key> INJECTED_PSI = Key.create("INJECTED_PSI"); - private static InjectionRegistrarImpl probeElementsUp(@NotNull PsiElement element, @NotNull PsiFile hostPsiFile, boolean probeUp) { + private static void probeElementsUp(@NotNull PsiElement element, + @NotNull PsiFile hostPsiFile, + boolean probeUp, + @NotNull PsiLanguageInjectionHost.InjectedPsiVisitor visitor) { PsiManager psiManager = hostPsiFile.getManager(); final Project project = psiManager.getProject(); InjectedLanguageManagerImpl injectedManager = InjectedLanguageManagerImpl.getInstanceImpl(project); - InjectionRegistrarImpl registrar = null; + InjectionResult result = null; PsiElement current = element; - nextParent: + while (current != null && current != hostPsiFile && !(current instanceof PsiDirectory)) { ProgressManager.checkCanceled(); if ("EL".equals(current.getLanguage().getID())) break; - ParameterizedCachedValue data = current.getUserData(INJECTED_PSI); - if (data == null || (registrar = data.getValue(current)) == null || !registrar.isValid()) { - registrar = InjectedPsiCachedValueProvider.doCompute(current, injectedManager, project, hostPsiFile); + ParameterizedCachedValue data = current.getUserData(INJECTED_PSI); + if (data == null || (result = data.getValue(current)) == null || !result.isValid()) { + result = InjectedPsiCachedValueProvider.doCompute(current, injectedManager, project, hostPsiFile); } current = current.getParent(); // cache no injection for current - if (registrar != null) { - List> places = registrar.getResult(); - // check that injections found intersect with queried element - TextRange elementRange = element.getTextRange(); - for (Pair pair : places) { - Place place = pair.first; - if (place.isValid()) { - for (PsiLanguageInjectionHost.Shred shred : place) { - PsiLanguageInjectionHost hostElement = shred.getHost(); - if (hostElement != null && hostElement.getTextRange().intersects(elementRange)) { - break nextParent; + if (result != null) { + if (result.files != null) { + for (PsiFile injectedPsiFile : result.files) { + Place place = getShreds(injectedPsiFile); + if (place.isValid()) { + // check that injections found intersect with queried element + boolean intersects = intersects(element, place); + if (intersects) { + visitor.visit(injectedPsiFile, place); } } } } + if (result.references != null && visitor instanceof InjectedReferenceVisitor) { + InjectedReferenceVisitor refVisitor = (InjectedReferenceVisitor)visitor; + for (Pair pair : result.references) { + Place place = pair.getSecond(); + if (place.isValid()) { + // check that injections found intersect with queried element + boolean intersects = intersects(element, place); + if (intersects) { + ReferenceInjector injector = pair.getFirst(); + refVisitor.visitInjectedReference(injector, place); + } + } + } + } + break; // found injection, stop } if (!probeUp) { break; @@ -364,22 +367,34 @@ public class InjectedLanguageUtil { // cache only if we walked all parents for (PsiElement e = element; e != current && e != null && e != hostPsiFile; e = e.getParent()) { ProgressManager.checkCanceled(); - if (registrar == null) { + if (result == null) { e.putUserData(INJECTED_PSI, null); } else { - ParameterizedCachedValue cachedValue = + PsiParameterizedCachedValue cachedValue = + (PsiParameterizedCachedValue) CachedValuesManager.getManager(project).createParameterizedCachedValue(INJECTED_PSI_PROVIDER, false); - CachedValueProvider.Result result = - CachedValueProvider.Result.create(registrar, PsiModificationTracker.MODIFICATION_COUNT, registrar); - ((PsiParameterizedCachedValue)cachedValue).setValue(result); + CachedValueProvider.Result cachedResult = CachedValueProvider.Result.create(result, PsiModificationTracker.MODIFICATION_COUNT, result); + cachedValue.setValue(cachedResult); e.putUserData(INJECTED_PSI, cachedValue); } } } - return registrar; + } + + private static boolean intersects(@NotNull PsiElement hostElement, @NotNull Place place) { + TextRange hostElementRange = hostElement.getTextRange(); + boolean intersects = false; + for (PsiLanguageInjectionHost.Shred shred : place) { + PsiLanguageInjectionHost shredHost = shred.getHost(); + if (shredHost != null && shredHost.getTextRange().intersects(hostElementRange)) { + intersects = true; + break; + } + } + return intersects; } /** diff --git a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectedPsiCachedValueProvider.java b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectedPsiCachedValueProvider.java index 3af0f8d659e5..bf55467c27be 100644 --- a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectedPsiCachedValueProvider.java +++ b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectedPsiCachedValueProvider.java @@ -32,12 +32,9 @@ import org.jetbrains.annotations.Nullable; import java.util.List; -/** - * @author cdr -*/ -class InjectedPsiCachedValueProvider implements ParameterizedCachedValueProvider { +class InjectedPsiCachedValueProvider implements ParameterizedCachedValueProvider { @Override - public CachedValueProvider.Result compute(PsiElement element) { + public CachedValueProvider.Result compute(PsiElement element) { PsiFile hostPsiFile = element.getContainingFile(); if (hostPsiFile == null) return null; FileViewProvider viewProvider = hostPsiFile.getViewProvider(); @@ -48,20 +45,20 @@ class InjectedPsiCachedValueProvider implements ParameterizedCachedValueProvider final Project project = psiManager.getProject(); InjectedLanguageManagerImpl injectedManager = InjectedLanguageManagerImpl.getInstanceImpl(project); - final InjectionRegistrarImpl result = doCompute(element, injectedManager, project, hostPsiFile); + InjectionResult result = doCompute(element, injectedManager, project, hostPsiFile); return CachedValueProvider.Result.create(result, PsiModificationTracker.MODIFICATION_COUNT, hostDocument); } @Nullable - static InjectionRegistrarImpl doCompute(@NotNull final PsiElement element, - @NotNull InjectedLanguageManagerImpl injectedManager, - Project project, - PsiFile hostPsiFile) { + static InjectionResult doCompute(@NotNull final PsiElement element, + @NotNull InjectedLanguageManagerImpl injectedManager, + @NotNull Project project, + @NotNull PsiFile hostPsiFile) { MyInjProcessor processor = new MyInjProcessor(project, hostPsiFile); injectedManager.processInPlaceInjectorsFor(element, processor); InjectionRegistrarImpl registrar = processor.hostRegistrar; - return registrar == null || registrar.getResult() == null ? null : registrar; + return registrar == null ? null : registrar.getInjectedResult(); } private static class MyInjProcessor implements InjectedLanguageManagerImpl.InjProcessor { @@ -69,7 +66,7 @@ class InjectedPsiCachedValueProvider implements ParameterizedCachedValueProvider private final Project myProject; private final PsiFile myHostPsiFile; - private MyInjProcessor(Project project, PsiFile hostPsiFile) { + private MyInjProcessor(@NotNull Project project, @NotNull PsiFile hostPsiFile) { myProject = project; myHostPsiFile = hostPsiFile; } 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 40cc878acb70..bdeb3f513829 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 @@ -53,6 +53,7 @@ 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; import org.jetbrains.annotations.NonNls; @@ -62,38 +63,42 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; -class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHostRegistrar, ModificationTracker { - private List> result; +@SuppressWarnings("deprecation") +class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHostRegistrar { + private List resultFiles; + private List> resultReferences; private Language myLanguage; - private List> escapers; - private List shreds; - private StringBuilder outChars; - private boolean isOneLineEditor; - private boolean cleared; + private List placeInfos; + private boolean cleared = true; private String fileExtension; private final Project myProject; - private final PsiManager myPsiManager; private final DocumentEx myHostDocument; private final VirtualFile myHostVirtualFile; private final PsiElement myContextElement; private final PsiFile myHostPsiFile; - private ReferenceInjector myReferenceInjector; + private Thread currentThread; InjectionRegistrarImpl(@NotNull Project project, @NotNull PsiFile hostPsiFile, @NotNull PsiElement contextElement) { myProject = project; myContextElement = contextElement; myHostPsiFile = PsiUtilCore.getTemplateLanguageFile(hostPsiFile); - myPsiManager = myHostPsiFile.getManager(); - cleared = true; FileViewProvider viewProvider = myHostPsiFile.getViewProvider(); myHostVirtualFile = viewProvider.getVirtualFile(); myHostDocument = (DocumentEx)viewProvider.getDocument(); } + @Override @Nullable("null means nobody cared to call .doneInjecting()") + @Deprecated public List> getResult() { - return result; + return resultFiles == null ? null : resultFiles.stream().map(file -> Pair.create(InjectedLanguageUtil.getShreds(file), file)).collect(Collectors.toList()); + } + + @Nullable + InjectionResult getInjectedResult() { + return resultFiles == null && resultReferences == null ? null : new InjectionResult(resultFiles, resultReferences); } @NotNull @@ -106,14 +111,13 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost @Override public MultiHostRegistrar startInjecting(@NotNull Language language, @Nullable String extension) { fileExtension = extension; - escapers = new SmartList<>(); - shreds = new SmartList<>(); - outChars = new StringBuilder(); + placeInfos = new SmartList<>(); if (!cleared) { clear(); throw new IllegalStateException("Seems you haven't called doneInjecting()"); } + currentThread = Thread.currentThread(); if (LanguageParserDefinitions.INSTANCE.forLanguage(language) == null) { throw new UnsupportedOperationException("Cannot inject language '" + language + "' because it has no ParserDefinition"); @@ -123,14 +127,38 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost } private void clear() { - escapers.clear(); - shreds.clear(); - outChars.setLength(0); - isOneLineEditor = false; fileExtension = null; myLanguage = null; cleared = true; + placeInfos = null; + 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 @@ -139,148 +167,93 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost @NonNls @Nullable String suffix, @NotNull PsiLanguageInjectionHost host, @NotNull TextRange rangeInsideHost) { - + checkThreading(); + if (myLanguage == null) { + clear(); + throw new IllegalStateException("Seems you haven't called startInjecting()"); + } PsiFile containingFile = PsiUtilCore.getTemplateLanguageFile(host); - assert containingFile == myHostPsiFile : exceptionContext("Trying to inject into foreign file: "+containingFile); + assert containingFile == myHostPsiFile : exceptionContext("Trying to inject into foreign file: "+containingFile, myLanguage, + myHostPsiFile, myHostVirtualFile, myHostDocument, myContextElement, placeInfos); TextRange hostTextRange = host.getTextRange(); if (!hostTextRange.contains(rangeInsideHost.shiftRight(hostTextRange.getStartOffset()))) { clear(); throw new IllegalArgumentException("rangeInsideHost must lie within host text range. rangeInsideHost:"+rangeInsideHost+"; host textRange:"+ hostTextRange); } - if (myLanguage == null && myReferenceInjector == null) { - clear(); - throw new IllegalStateException("Seems you haven't called startInjecting()"); - } - if (prefix == null) prefix = ""; - if (suffix == null) suffix = ""; cleared = false; + PlaceInfo info = new PlaceInfo(ObjectUtils.notNull(prefix, ""), ObjectUtils.notNull(suffix, ""), host, rangeInsideHost); + placeInfos.add(info); + + return this; + } + + private void checkThreading() { + if (currentThread != Thread.currentThread()) { + throw new IllegalStateException("Wow, you must not start injecting in one thread ("+currentThread+") but finish the other"); + } + } + + @NotNull + private static Pair.NonNull createShred(@NotNull Project project, @NotNull PlaceInfo info, + @NotNull StringBuilder outChars, + @NotNull PsiFile hostPsiFile) { int startOffset = outChars.length(); + String prefix = info.prefix; outChars.append(prefix); + PsiLanguageInjectionHost host = info.host; LiteralTextEscaper textEscaper = host.createLiteralTextEscaper(); - escapers.add(textEscaper); - isOneLineEditor |= textEscaper.isOneLine(); + + TextRange rangeInsideHost = info.rangeInsideHost; TextRange relevantRange = textEscaper.getRelevantTextRange().intersection(rangeInsideHost); if (relevantRange == null) { relevantRange = TextRange.from(textEscaper.getRelevantTextRange().getStartOffset(), 0); } else { int before = outChars.length(); - boolean result = textEscaper.decode(relevantRange, outChars); + boolean decodeFailed = !textEscaper.decode(relevantRange, outChars); int after = outChars.length(); assert after >= before : "Escaper " + textEscaper + "("+textEscaper.getClass()+") must not mangle char buffer"; - if (!result) { + if (decodeFailed) { // if there are invalid chars, adjust the range int offsetInHost = textEscaper.getOffsetInHost(outChars.length() - before, rangeInsideHost); relevantRange = relevantRange.intersection(new ProperTextRange(0, offsetInHost)); } } + String suffix = info.suffix; outChars.append(suffix); int endOffset = outChars.length(); + TextRange hostTextRange = host.getTextRange(); TextRange relevantRangeInHost = relevantRange.shiftRight(hostTextRange.getStartOffset()); - SmartPointerManagerImpl manager = (SmartPointerManagerImpl)SmartPointerManager.getInstance(myProject); - shreds.add(new ShredImpl(manager.createSmartPsiFileRangePointer(myHostPsiFile, relevantRangeInHost, true), - manager.createSmartPsiElementPointer(host, myHostPsiFile, true), - prefix, suffix, new ProperTextRange(startOffset, endOffset), false)); - return this; + 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); + } @Override public void doneInjecting() { + checkThreading(); try { - if (shreds.isEmpty()) { + if (myLanguage == null) { + throw new IllegalStateException("Seems you haven't called startInjecting()"); + } + if (placeInfos.isEmpty()) { throw new IllegalStateException("Seems you haven't called addPlace()"); } - if (myReferenceInjector != null) { - addToResults(new Place(shreds), null); - return; - } - PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(myProject); - - Place place = new Place(shreds); - DocumentWindowImpl documentWindow = new DocumentWindowImpl(myHostDocument, isOneLineEditor, place); - String fileName = PathUtil.makeFileName(myHostVirtualFile.getName(), fileExtension); - VirtualFileWindowImpl virtualFile = new VirtualFileWindowImpl(fileName, myHostVirtualFile, documentWindow, myLanguage, outChars); - Language forcedLanguage = myContextElement.getUserData(InjectedFileViewProvider.LANGUAGE_FOR_INJECTED_COPY_KEY); - myLanguage = forcedLanguage == null ? LanguageSubstitutors.INSTANCE.substituteLanguage(myLanguage, virtualFile, myProject) : forcedLanguage; - - createDocument(virtualFile); - - InjectedFileViewProvider viewProvider = new InjectedFileViewProvider(myPsiManager, virtualFile, documentWindow, myLanguage); - ParserDefinition parserDefinition = LanguageParserDefinitions.INSTANCE.forLanguage(myLanguage); - assert parserDefinition != null : "Parser definition for language "+myLanguage+" is null"; - PsiFile psiFile = parserDefinition.createFile(viewProvider); - - SmartPsiElementPointer pointer = ((ShredImpl)shreds.get(0)).getSmartPointer(); - synchronized (InjectedLanguageManagerImpl.ourInjectionPsiLock) { - 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(outChars) : exceptionContext("Before patch: doc:\n'" + documentText + "'\n---PSI:\n'" + parsedNode.getText() + "'\n---chars:\n'"+outChars+"'"); - - viewProvider.setPatchingLeaves(true); - try { - patchLeaves(parsedNode, escapers, place); - } - catch (ProcessCanceledException e) { - throw e; - } - catch (RuntimeException e) { - throw new RuntimeException(exceptionContext("Patch error"), e); - } - finally { - viewProvider.setPatchingLeaves(false); - } - if (!((FileElement)parsedNode).textMatches(documentText)) { - throw new AssertionError(exceptionContext("After patch: doc:\n'" + documentText + "'\n---PSI:\n'" + parsedNode.getText() + "'\n---chars:\n'" + outChars + "'")); - } - - virtualFile.setContent(null, documentWindow.getText(), false); - virtualFile.setWritable(virtualFile.getDelegate().isWritable()); - - cacheEverything(place, documentWindow, viewProvider, psiFile, pointer); - - PsiFile cachedPsiFile = documentManager.getCachedPsiFile(documentWindow); - assert cachedPsiFile == psiFile : "Cached psi :"+ cachedPsiFile +" instead of "+psiFile; - - assert place.isValid(); - assert viewProvider.isValid(); - PsiFile newFile = registerDocument(documentWindow, psiFile, place, myHostPsiFile, documentManager); - boolean mergeHappened = newFile != psiFile; - if (mergeHappened) { - InjectedLanguageUtil.clearCaches(psiFile, documentWindow); - psiFile = newFile; - viewProvider = (InjectedFileViewProvider)psiFile.getViewProvider(); - documentWindow = (DocumentWindowImpl)viewProvider.getDocument(); - virtualFile = (VirtualFileWindowImpl)viewProvider.getVirtualFile(); - boolean shredsRewritten = cacheEverything(place, documentWindow, viewProvider, psiFile, pointer); - if (!shredsRewritten) { - place.dispose(); - place = documentWindow.getShreds(); - } - } - - assert psiFile.isValid(); - assert place.isValid(); - assert viewProvider.isValid(); - - try { - List, TextRange>> tokens = obtainHighlightTokensFromLexer(myLanguage, outChars, escapers, place, virtualFile, myProject); - psiFile.putUserData(InjectedLanguageUtil.HIGHLIGHT_TOKENS, tokens); - } - catch (ProcessCanceledException e) { - throw e; - } - catch (RuntimeException e) { - throw new RuntimeException(exceptionContext("Obtaining tokens error"), e); - } - - addToResults(place, psiFile); + Language forcedLanguage = myContextElement.getUserData(InjectedFileViewProvider.LANGUAGE_FOR_INJECTED_COPY_KEY); + PsiFile psiFile = + createInjectedFile(myProject, myLanguage, forcedLanguage, + myHostDocument, myHostVirtualFile, myHostPsiFile, fileExtension, placeInfos, + myContextElement); + addFileToResults(psiFile); + PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(myProject); + DocumentWindowImpl documentWindow = (DocumentWindowImpl)documentManager.getDocument(psiFile); assertEverythingIsAllright(documentManager, documentWindow, psiFile); } } @@ -289,6 +262,124 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost } } + @NotNull + private static PsiFile createInjectedFile(@NotNull Project project, + @NotNull Language language, @Nullable Language forcedLanguage, + @NotNull DocumentEx hostDocument, + @NotNull VirtualFile hostVirtualFile, + @NotNull PsiFile hostPsiFile, + @Nullable String injectedFileExtension, + @NotNull List placeInfos, + @NotNull PsiElement contextElement) { + 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); + 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); + + 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); + + PsiFile cachedPsiFile = documentManager.getCachedPsiFile(documentWindow); + assert cachedPsiFile == psiFile : "Cached psi :"+ cachedPsiFile +" instead of "+psiFile; + + assert place.isValid(); + assert viewProvider.isValid(); + + PsiFile newFile = registerDocument(documentWindow, psiFile, place, hostPsiFile, documentManager); + boolean mergeHappened = newFile != psiFile; + Place mergedPlace = place; + if (mergeHappened) { + InjectedLanguageUtil.clearCaches(psiFile, documentWindow); + psiFile = newFile; + viewProvider = (InjectedFileViewProvider)psiFile.getViewProvider(); + documentWindow = (DocumentWindowImpl)viewProvider.getDocument(); + virtualFile = (VirtualFileWindowImpl)viewProvider.getVirtualFile(); + boolean shredsReused = !cacheEverything(place, documentWindow, viewProvider, psiFile, pointer); + if (shredsReused) { + place.dispose(); + mergedPlace = documentWindow.getShreds(); + } + } + + 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, @@ -302,21 +393,28 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost if (injector == null) { throw new IllegalArgumentException("Language "+language+" being injected as reference must register reference injector"); } - escapers = new SmartList<>(); - shreds = new SmartList<>(); - outChars = new StringBuilder(); + placeInfos = new SmartList<>(); if (!cleared) { clear(); throw new IllegalStateException("Seems you haven't called doneInjecting()"); } - myReferenceInjector = injector; myLanguage = language; + currentThread = Thread.currentThread(); addPlace(prefix, suffix, host, rangeInsideHost); - doneInjecting(); + Place place = new Place(); + StringBuilder decodedChars = new StringBuilder(); + Pair.NonNull p = createShred(myProject, placeInfos.get(0), decodedChars, myHostPsiFile); + place.add(p.getFirst()); + if (resultReferences == null) { + resultReferences = new SmartList<>(); + } + resultReferences.add(Pair.create(injector, place)); + clear(); } + private static void createDocument(@NotNull LightVirtualFile virtualFile) { CharSequence content = virtualFile.getContent(); @@ -345,18 +443,24 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost keepTreeFromChameleoningBack(psiFile); - return viewProvider.setShreds(place, psiFile.getProject()); + return viewProvider.setShreds(place); } @NonNls - private String exceptionContext(@NonNls String msg) { + private static String exceptionContext(@NonNls @NotNull String msg, + @NotNull Language language, + @NotNull PsiFile hostPsiFile, + @NotNull VirtualFile hostVirtualFile, + @NotNull DocumentEx hostDocument, + @NotNull PsiElement contextElement, + @NotNull List placeInfos) { return msg + ".\n" + - myLanguage+";\n "+ - "Host file: "+myHostPsiFile+" in '" + myHostVirtualFile.getPresentableUrl() + "'" + - (PsiDocumentManager.getInstance(myProject).isUncommited(myHostDocument) ? " (uncommitted)": "")+ "\n" + - "Context element "+myContextElement.getTextRange() + ": '" + myContextElement +"'; "+ - "Ranges: "+shreds; + language + ";\n " + + "Host file: " + hostPsiFile + " in '" + hostVirtualFile.getPresentableUrl() + "'" + + (PsiDocumentManager.getInstance(hostPsiFile.getProject()).isUncommited(hostDocument) ? " (uncommitted)" : "") + "\n" + + "Context element " + contextElement.getTextRange() + ": '" + contextElement + "'; " + + "Ranges: " + placeInfos; } private static final Key TREE_HARD_REF = Key.create("TREE_HARD_REF"); @@ -377,14 +481,9 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost return node; } - private void assertEverythingIsAllright(PsiDocumentManagerBase documentManager, DocumentWindowImpl documentWindow, PsiFile psiFile) { - boolean isAncestor = false; - for (PsiLanguageInjectionHost.Shred shred : shreds) { - PsiLanguageInjectionHost host = shred.getHost(); - isAncestor |= PsiTreeUtil.isAncestor(myContextElement, host, false); - } - assert isAncestor : exceptionContext(myContextElement + " must be the parent of at least one of injection hosts"); - + private static void assertEverythingIsAllright(@NotNull PsiDocumentManagerBase documentManager, + @NotNull DocumentWindowImpl documentWindow, + @NotNull PsiFile psiFile) { InjectedFileViewProvider injectedFileViewProvider = (InjectedFileViewProvider)psiFile.getViewProvider(); assert injectedFileViewProvider.isValid() : "Invalid view provider: "+injectedFileViewProvider; DocumentEx frozenWindow = documentManager.getLastCommittedDocument(documentWindow); @@ -398,22 +497,36 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost PsiDocumentManagerBase.checkConsistency(psiFile, frozenWindow); } - void addToResults(@NotNull Place place, @Nullable /*WAT?*/PsiFile psiFile, @NotNull InjectionRegistrarImpl from) { - addToResults(place, psiFile); - myReferenceInjector = from.myReferenceInjector; + void addToResults(@NotNull InjectionResult result) { + if (result.files != null) { + for (PsiFile file : result.files) { + addFileToResults(file); + } + } + if (result.references != null) { + for (Pair pair : result.references) { + addReferenceToResults(pair); + } + } } - private void addToResults(@NotNull Place place, @Nullable /*WAT?*/PsiFile psiFile) { - if (result == null) { - result = new SmartList<>(); + private void addFileToResults(@NotNull PsiFile psiFile) { + if (resultFiles == null) { + resultFiles = new SmartList<>(); } - result.add(Pair.create(place, psiFile)); + resultFiles.add(psiFile); + } + private void addReferenceToResults(@NotNull Pair pair) { + if (resultReferences == null) { + resultReferences = new SmartList<>(); + } + resultReferences.add(pair); } private static void patchLeaves(@NotNull ASTNode parsedNode, - @NotNull List> escapers, - @NotNull Place shreds) { + @NotNull Place shreds, + @NotNull List escapers) { LeafPatcher patcher = new LeafPatcher(shreds, escapers); ((TreeElement)parsedNode).acceptTree(patcher); @@ -524,20 +637,21 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost } // returns lexer element types with corresponding ranges in encoded (injection host based) PSI + @NotNull private static List, TextRange>> - obtainHighlightTokensFromLexer(Language language, - StringBuilder outChars, - List> escapers, - Place shreds, - VirtualFileWindow virtualFile, - Project project) { + obtainHighlightTokensFromLexer(@NotNull Language language, + @NotNull CharSequence outChars, + @NotNull Place shreds, + @NotNull VirtualFileWindow virtualFile, + @NotNull Project project, + @NotNull List escapers) { SyntaxHighlighter syntaxHighlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(language, project, (VirtualFile)virtualFile); Lexer lexer = syntaxHighlighter.getHighlightingLexer(); lexer.start(outChars); int hostNum = -1; int prevHostEndOffset = 0; - SmartPsiElementPointer host = null; - LiteralTextEscaper escaper = null; + SmartPsiElementPointer hostPtr = null; + LiteralTextEscaper escaper = null; int prefixLength = 0; int suffixLength = 0; TextRange rangeInsideHost = null; @@ -551,7 +665,7 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost PsiLanguageInjectionHost.Shred shred = shreds.get(hostNum); shredEndOffset = shred.getRange().getEndOffset(); prevHostEndOffset = range.getStartOffset(); - host = ((ShredImpl)shred).getSmartPointer(); + hostPtr = ((ShredImpl)shred).getSmartPointer(); escaper = escapers.get(hostNum); rangeInsideHost = shred.getRangeInsideHost(); prefixLength = shred.getPrefix().length(); @@ -575,7 +689,7 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost prevHostEndOffset = shredEndOffset; } TextRange rangeInHost = new ProperTextRange(start, end); - tokens.add(Trinity.create(tokenType, host, rangeInHost)); + tokens.add(Trinity.create(tokenType, hostPtr, rangeInHost)); } range = spilled; } @@ -583,22 +697,9 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost return tokens; } - // for CachedValue - @Override - public long getModificationCount() { - List shredList = shreds; - if (shredList != null) { - for (PsiLanguageInjectionHost.Shred shred : shredList) { - if (!shred.isValid()) return -1; - } - } - DocumentEx hostDocument = myHostDocument; - return hostDocument == null ? -1 : hostDocument.getModificationStamp(); - } - @Override public String toString() { - return String.valueOf(result); + return String.valueOf(resultFiles); } // performance: avoid context.getContainingFile() @@ -606,17 +707,4 @@ class InjectionRegistrarImpl extends MultiHostRegistrarImpl implements MultiHost PsiFile getHostPsiFile() { return myHostPsiFile; } - - ReferenceInjector getReferenceInjector() { - return myReferenceInjector; - } - - boolean isValid() { - List> result = getResult(); - if (result == null) return false; - for (Pair pair : result) { - if (!pair.getFirst().isValid()) return false; - } - return true; - } } diff --git a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectionResult.java b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectionResult.java new file mode 100644 index 000000000000..4618b6cf763d --- /dev/null +++ b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/InjectionResult.java @@ -0,0 +1,61 @@ +// 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. +package com.intellij.psi.impl.source.tree.injected; + +import com.intellij.openapi.util.ModificationTracker; +import com.intellij.openapi.util.Pair; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiLanguageInjectionHost; +import com.intellij.psi.injection.ReferenceInjector; + +import java.util.List; + +class InjectionResult implements ModificationTracker { + final List files; + final List> references; + + InjectionResult(List files, List> references) { + this.files = files; + this.references = references; + if (files == null && references == null) throw new IllegalArgumentException("At least one argument must not be null"); + } + + boolean isValid() { + if (files != null) { + for (PsiFile file : files) { + if (!file.isValid()) return false; + } + } + else { + for (Pair pair : references) { + Place place = pair.getSecond(); + if (!place.isValid()) return false; + } + } + return true; + } + + // for CachedValue + @Override + public long getModificationCount() { + long modCount = 0; + if (files != null) { + for (PsiFile file : files) { + if (!file.isValid()) return -1; + modCount += file.getModificationStamp(); + } + } + if (references != null) { + for (Pair pair : references) { + Place place = pair.getSecond(); + if (!place.isValid()) return -1; + for (PsiLanguageInjectionHost.Shred shred : place) { + PsiLanguageInjectionHost host = shred.getHost(); + if (host == null || !host.isValid()) return -1; + PsiFile file = host.getContainingFile(); + modCount += file.getModificationStamp(); + } + } + } + return modCount; + } +} 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 d12cf91935ed..171d4b1d1c54 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 @@ -26,6 +26,7 @@ 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 gnu.trove.THashMap; +import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.Map; @@ -36,13 +37,14 @@ import java.util.Map; class LeafPatcher extends RecursiveTreeElementWalkingVisitor { private int shredNo; private String hostText; + private LiteralTextEscaper currentTextEscaper; private TextRange rangeInHost; private final Place myShreds; - private final List> myEscapers; + @NotNull private final List myEscapers; final Map newTexts = new THashMap<>(); final StringBuilder catLeafs = new StringBuilder(); - LeafPatcher(Place shreds, List> escapers) { + LeafPatcher(@NotNull Place shreds, @NotNull List escapers) { myShreds = shreds; myEscapers = escapers; } @@ -65,8 +67,10 @@ class LeafPatcher extends RecursiveTreeElementWalkingVisitor { boolean firstTimer = false; PsiLanguageInjectionHost.Shred current = myShreds.get(shredNo); if (hostText == null) { - hostText = current.getHost().getText(); + PsiLanguageInjectionHost host = current.getHost(); + hostText = host.getText(); rangeInHost = current.getRangeInsideHost(); + currentTextEscaper = myEscapers.get(shredNo); firstTimer = true; } @@ -76,7 +80,9 @@ class LeafPatcher extends RecursiveTreeElementWalkingVisitor { String prefix = current.getPrefix(); if (startOffset >= shredRange.getEndOffset()) { current = myShreds.get(++shredNo); - hostText = current.getHost().getText(); + PsiLanguageInjectionHost host = current.getHost(); + hostText = host.getText(); + currentTextEscaper = myEscapers.get(shredNo); rangeInHost = current.getRangeInsideHost(); firstTimer = true; continue; @@ -93,10 +99,10 @@ class LeafPatcher extends RecursiveTreeElementWalkingVisitor { String suffix = current.getSuffix(); if (startOffset < shredRange.getEndOffset() - suffix.length()) { // inside host body, cut out from the host text - int startOffsetInHost = myEscapers.get(shredNo).getOffsetInHost( + int startOffsetInHost = currentTextEscaper.getOffsetInHost( startOffset - shredRange.getStartOffset() - prefix.length(), rangeInHost); int endOffsetCut = Math.min(endOffset, shredRange.getEndOffset() - suffix.length()); - int endOffsetInHost = myEscapers.get(shredNo).getOffsetInHost( + int endOffsetInHost = currentTextEscaper.getOffsetInHost( endOffsetCut - shredRange.getStartOffset() - prefix.length(), rangeInHost); if (endOffsetInHost != -1) { if (firstTimer ) text.append(hostText, rangeInHost.getStartOffset(), startOffsetInHost); diff --git a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/Place.java b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/Place.java index d4a0ce5b50ae..51a92c43bf4e 100644 --- a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/Place.java +++ b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/injected/Place.java @@ -21,11 +21,8 @@ import com.intellij.psi.SmartPsiElementPointer; import com.intellij.util.SmartList; import org.jetbrains.annotations.NotNull; -import java.util.List; - public class Place extends SmartList { - Place(@NotNull List shreds) { - super(shreds); + Place() { } @NotNull 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 eaf9560627c1..35bc824b5ed2 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 @@ -29,19 +29,21 @@ class ShredImpl implements PsiLanguageInjectionHost.Shred { private final String prefix; private final String suffix; private final boolean usePsiRange; + private final boolean isOneLine; ShredImpl(@NotNull SmartPsiFileRange relevantRangeInHost, @NotNull SmartPsiElementPointer hostElementPointer, @NotNull String prefix, @NotNull String suffix, @NotNull TextRange range, - boolean usePsiRange) { + boolean usePsiRange, boolean isOneLine) { this.hostElementPointer = hostElementPointer; this.relevantRangeInHost = relevantRangeInHost; this.prefix = prefix; this.suffix = suffix; this.range = range; this.usePsiRange = usePsiRange; + this.isOneLine = isOneLine; assertValid(); } @@ -55,7 +57,7 @@ class ShredImpl implements PsiLanguageInjectionHost.Shred { } ShredImpl withPsiRange() { - return new ShredImpl(relevantRangeInHost, hostElementPointer, prefix, suffix, range, true); + return new ShredImpl(relevantRangeInHost, hostElementPointer, prefix, suffix, range, true, isOneLine); } @NotNull @@ -155,4 +157,8 @@ class ShredImpl implements PsiLanguageInjectionHost.Shred { public String getSuffix() { return suffix; } + + boolean isOneLine() { + return isOneLine; + } }