/* * Copyright 2000-2009 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.psi.impl.source.tree.injected; import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; import com.intellij.codeInsight.daemon.impl.DaemonProgressIndicator; import com.intellij.concurrency.JobLauncher; import com.intellij.injected.editor.DocumentWindow; import com.intellij.injected.editor.DocumentWindowImpl; import com.intellij.injected.editor.EditorWindowImpl; import com.intellij.injected.editor.VirtualFileWindow; import com.intellij.lang.Language; import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.lang.injection.MultiHostInjector; import consulo.lang.injection.MultiHostInjectorExtensionPoint; import com.intellij.lang.injection.MultiHostRegistrar; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ex.ApplicationManagerEx; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.extensions.ExtensionPoint; import com.intellij.openapi.extensions.ExtensionPointListener; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.extensions.PluginDescriptor; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.ProperTextRange; import com.intellij.openapi.util.Segment; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.impl.PsiDocumentManagerImpl; import com.intellij.psi.impl.source.resolve.FileContextUtil; import com.intellij.util.ArrayUtil; import com.intellij.util.Processor; import com.intellij.util.SmartList; import com.intellij.util.containers.ConcurrentList; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.MultiMap; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import java.util.*; import java.util.concurrent.atomic.AtomicReference; /** * @author cdr */ public class InjectedLanguageManagerImpl extends InjectedLanguageManager implements Disposable { private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.tree.injected.InjectedLanguageManagerImpl"); private final Project myProject; private final DumbService myDumbService; private final AtomicReference<MultiHostInjector> myPsiManagerRegisteredInjectorsAdapter = new AtomicReference<MultiHostInjector>(); private volatile DaemonProgressIndicator myProgress; public static InjectedLanguageManagerImpl getInstanceImpl(Project project) { return (InjectedLanguageManagerImpl)InjectedLanguageManager.getInstance(project); } public InjectedLanguageManagerImpl(final Project project, DumbService dumbService) { myProject = project; myDumbService = dumbService; final ExtensionPoint<MultiHostInjectorExtensionPoint> multiPoint = Extensions.getArea(project).getExtensionPoint(MultiHostInjector.EP_NAME); multiPoint.addExtensionPointListener(new ExtensionPointListener<MultiHostInjectorExtensionPoint>() { @Override public void extensionAdded(@NotNull MultiHostInjectorExtensionPoint ep, @Nullable PluginDescriptor pluginDescriptor) { registerMultiHostInjector(ep.getInstance(project), ep.getKey()); } @Override public void extensionRemoved(@NotNull MultiHostInjectorExtensionPoint ep, @Nullable PluginDescriptor pluginDescriptor) { unregisterMultiHostInjector(ep.getInstance(project)); } }, this); final ExtensionPointListener<LanguageInjector> myListener = new ExtensionPointListener<LanguageInjector>() { @Override public void extensionAdded(@NotNull LanguageInjector extension, @Nullable PluginDescriptor pluginDescriptor) { psiManagerInjectorsChanged(); } @Override public void extensionRemoved(@NotNull LanguageInjector extension, @Nullable PluginDescriptor pluginDescriptor) { psiManagerInjectorsChanged(); } }; final ExtensionPoint<LanguageInjector> psiManagerPoint = Extensions.getRootArea().getExtensionPoint(LanguageInjector.EXTENSION_POINT_NAME); psiManagerPoint.addExtensionPointListener(myListener, this); myProgress = new DaemonProgressIndicator(); project.getMessageBus().connect(this) .subscribe(DaemonCodeAnalyzer.DAEMON_EVENT_TOPIC, new DaemonCodeAnalyzer.DaemonListenerAdapter() { @Override public void daemonCancelEventOccurred(@NotNull String reason) { myProgress.cancel(); } }); } @Override public void dispose() { EditorWindowImpl.disposeInvalidEditors(); } @NotNull @Override public List<DocumentWindow> getCachedInjectedDocuments(@NotNull PsiFile hostPsiFile) { return InjectedLanguageUtil.getCachedInjectedDocuments(hostPsiFile); } @Override public void enumerate(@NotNull PsiElement host, @NotNull PsiLanguageInjectionHost.InjectedPsiVisitor visitor) { InjectedLanguageUtil.enumerate(host, visitor); } @Override public void enumerateEx(@NotNull PsiElement host, @NotNull PsiFile containingFile, boolean probeUp, @NotNull PsiLanguageInjectionHost.InjectedPsiVisitor visitor) { InjectedLanguageUtil.enumerate(host, containingFile, probeUp, visitor); } @Override public void startRunInjectors(@NotNull final Document hostDocument, final boolean synchronously) { if (myProject.isDisposed()) return; if (!synchronously && ApplicationManager.getApplication().isWriteAccessAllowed()) return; // use cached to avoid recreate PSI in alien project final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myProject); final PsiFile hostPsiFile = documentManager.getCachedPsiFile(hostDocument); if (hostPsiFile == null) return; final ConcurrentList<DocumentWindow> injected = InjectedLanguageUtil.getCachedInjectedDocuments(hostPsiFile); if (injected.isEmpty()) return; if (myProgress.isCanceled()) { myProgress = new DaemonProgressIndicator(); } final Processor<DocumentWindow> commitProcessor = new Processor<DocumentWindow>() { @Override public boolean process(DocumentWindow documentWindow) { if (myProject.isDisposed()) return false; ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); if (indicator != null && indicator.isCanceled()) return false; if (documentManager.isUncommited(hostDocument) || !hostPsiFile.isValid()) { return false; // will be committed later } Segment[] ranges = documentWindow.getHostRanges(); Segment rangeMarker = ranges.length > 0 ? ranges[0] : null; PsiElement element = rangeMarker == null ? null : hostPsiFile.findElementAt(rangeMarker.getStartOffset()); if (element == null) { synchronized (PsiLock.LOCK) { injected.remove(documentWindow); } return true; } final DocumentWindow[] stillInjectedDocument = {null}; // it is here where the reparse happens and old file contents replaced InjectedLanguageUtil.enumerate(element, hostPsiFile, true, new PsiLanguageInjectionHost.InjectedPsiVisitor() { @Override public void visit(@NotNull PsiFile injectedPsi, @NotNull List<PsiLanguageInjectionHost.Shred> places) { stillInjectedDocument[0] = (DocumentWindow)injectedPsi.getViewProvider().getDocument(); PsiDocumentManagerImpl.checkConsistency(injectedPsi, stillInjectedDocument[0]); } }); synchronized (PsiLock.LOCK) { if (stillInjectedDocument[0] == null) { injected.remove(documentWindow); } else if (stillInjectedDocument[0] != documentWindow) { injected.remove(documentWindow); injected.addIfAbsent(stillInjectedDocument[0]); } } return true; } }; final Runnable commitInjectionsRunnable = new Runnable() { @Override public void run() { if (myProgress.isCanceled()) return; JobLauncher.getInstance() .invokeConcurrentlyUnderProgress(new ArrayList<DocumentWindow>(injected), myProgress, !synchronously, commitProcessor); } }; if (synchronously) { if (Thread.holdsLock(PsiLock.LOCK)) { // hack for the case when docCommit was called from within PSI modification, e.g. in formatter. // we can't spawn threads to do injections there, otherwise a deadlock is imminent ContainerUtil.process(new ArrayList<DocumentWindow>(injected), commitProcessor); } else { commitInjectionsRunnable.run(); } } else { JobLauncher.getInstance().submitToJobThread(new Runnable() { @Override public void run() { ApplicationManagerEx.getApplicationEx().tryRunReadAction(commitInjectionsRunnable); } }, null); } } public void psiManagerInjectorsChanged() { LanguageInjector[] extensions = Extensions.getExtensions(LanguageInjector.EXTENSION_POINT_NAME); if (extensions.length == 0) { MultiHostInjector prev = myPsiManagerRegisteredInjectorsAdapter.getAndSet(null); if (prev != null) { unregisterMultiHostInjector(prev); } } else { PsiManagerRegisteredInjectorsAdapter adapter = new PsiManagerRegisteredInjectorsAdapter(); if (myPsiManagerRegisteredInjectorsAdapter.compareAndSet(null, adapter)) { registerMultiHostInjector(adapter, PsiLanguageInjectionHost.class); } } } @NotNull @Override public List<TextRange> getNonEditableFragments(@NotNull DocumentWindow window) { List<TextRange> result = ContainerUtil.newArrayList(); int offset = 0; for (PsiLanguageInjectionHost.Shred shred : ((DocumentWindowImpl)window).getShreds()) { Segment hostRange = shred.getHostRangeMarker(); if (hostRange == null) continue; offset = appendRange(result, offset, shred.getPrefix().length()); offset += hostRange.getEndOffset() - hostRange.getStartOffset(); offset = appendRange(result, offset, shred.getSuffix().length()); } return result; } private static int appendRange(List<TextRange> result, int start, int length) { if (length > 0) { int lastIndex = result.size() - 1; TextRange lastRange = lastIndex >= 0 ? result.get(lastIndex) : null; if (lastRange != null && lastRange.getEndOffset() == start) { result.set(lastIndex, lastRange.grown(length)); } else { result.add(TextRange.from(start, length)); } } return start + length; } @Override public PsiLanguageInjectionHost getInjectionHost(@NotNull PsiElement element) { final PsiFile file = element.getContainingFile(); final VirtualFile virtualFile = file == null ? null : file.getVirtualFile(); if (virtualFile instanceof VirtualFileWindow) { PsiElement host = FileContextUtil.getFileContext(file); // use utility method in case the file's overridden getContext() if (host instanceof PsiLanguageInjectionHost) { return (PsiLanguageInjectionHost)host; } } return null; } @Override @NotNull public TextRange injectedToHost(@NotNull PsiElement injectedContext, @NotNull TextRange injectedTextRange) { ProperTextRange.assertProperRange(injectedTextRange); PsiFile file = injectedContext.getContainingFile(); if (file == null) return injectedTextRange; Document document = PsiDocumentManager.getInstance(file.getProject()).getCachedDocument(file); if (!(document instanceof DocumentWindowImpl)) return injectedTextRange; DocumentWindowImpl documentWindow = (DocumentWindowImpl)document; return documentWindow.injectedToHost(injectedTextRange); } @Override public int injectedToHost(@NotNull PsiElement element, int offset) { PsiFile file = element.getContainingFile(); if (file == null) return offset; Document document = PsiDocumentManager.getInstance(file.getProject()).getCachedDocument(file); if (!(document instanceof DocumentWindowImpl)) return offset; DocumentWindowImpl documentWindow = (DocumentWindowImpl)document; return documentWindow.injectedToHost(offset); } // used only from tests => no need for complex synchronization private final MultiMap<Class<? extends PsiElement>, MultiHostInjector> myManualInjectors = MultiMap.createLinkedSet(); private volatile ClassMapCachingNulls<MultiHostInjector> cachedInjectors; public void processInjectableElements(Collection<PsiElement> in, Processor<PsiElement> processor) { ClassMapCachingNulls<MultiHostInjector> map = getInjectorMap(); for (PsiElement element : in) { if (map.get(element.getClass()) != null) processor.process(element); } } private ClassMapCachingNulls<MultiHostInjector> getInjectorMap() { ClassMapCachingNulls<MultiHostInjector> cached = cachedInjectors; if (cached != null) { return cached; } Map<Class, MultiHostInjector[]> injectors = ContainerUtil.newHashMap(); MultiMap<Class<? extends PsiElement>, MultiHostInjector> allInjectors = MultiMap.createLinkedSet(); allInjectors.putAllValues(myManualInjectors); for (MultiHostInjectorExtensionPoint multiHostInjectorExtensionPoint : MultiHostInjector.EP_NAME .getExtensions(myProject)) { Class<PsiElement> key = multiHostInjectorExtensionPoint.getKey(); MultiHostInjector multiHostInjector = multiHostInjectorExtensionPoint.getInstance(myProject); allInjectors.putValue(key, multiHostInjector); } if (LanguageInjector.EXTENSION_POINT_NAME.getExtensions().length > 0) { allInjectors.putValue(PsiLanguageInjectionHost.class, PsiManagerRegisteredInjectorsAdapter.INSTANCE); } for (Map.Entry<Class<? extends PsiElement>, Collection<MultiHostInjector>> entry : allInjectors.entrySet()) { Class<? extends PsiElement> place = entry.getKey(); Collection<MultiHostInjector> value = entry.getValue(); for (MultiHostInjector injector : value) { MultiHostInjector[] existing = injectors.get(place); injectors.put(place, existing == null ? new MultiHostInjector[]{injector} : ArrayUtil.append(existing, injector)); } } ClassMapCachingNulls<MultiHostInjector> result = new ClassMapCachingNulls<MultiHostInjector>(injectors, new MultiHostInjector[0]); cachedInjectors = result; return result; } private void clearInjectorCache() { cachedInjectors = null; } @Override public void registerMultiHostInjector(@NotNull MultiHostInjector injector, @NotNull Class<? extends PsiElement>... elements) { for (Class<? extends PsiElement> element : elements) { myManualInjectors.putValue(element, injector); } clearInjectorCache(); } @Override public boolean unregisterMultiHostInjector(@NotNull MultiHostInjector injector) { try { return myManualInjectors.values().remove(injector); } finally { clearInjectorCache(); } } @Override public String getUnescapedText(@NotNull final PsiElement injectedNode) { final StringBuilder text = new StringBuilder(injectedNode.getTextLength()); // gather text from (patched) leaves injectedNode.accept(new PsiRecursiveElementWalkingVisitor() { @Override public void visitElement(PsiElement element) { String leafText = InjectedLanguageUtil.getUnescapedLeafText(element, false); if (leafText != null) { text.append(leafText); return; } super.visitElement(element); } }); return text.toString(); } /** * intersection may spread over several injected fragments * * @param rangeToEdit range in encoded(raw) PSI * @return list of ranges in encoded (raw) PSI */ @Override @SuppressWarnings({"ConstantConditions", "unchecked"}) @NotNull public List<TextRange> intersectWithAllEditableFragments(@NotNull PsiFile injectedPsi, @NotNull TextRange rangeToEdit) { Place shreds = InjectedLanguageUtil.getShreds(injectedPsi); if (shreds == null) return Collections.emptyList(); Object result = null; // optimization: TextRange or ArrayList int count = 0; int offset = 0; for (PsiLanguageInjectionHost.Shred shred : shreds) { TextRange encodedRange = TextRange.from(offset + shred.getPrefix().length(), shred.getRangeInsideHost().getLength()); TextRange intersection = encodedRange.intersection(rangeToEdit); if (intersection != null) { count++; if (count == 1) { result = intersection; } else if (count == 2) { TextRange range = (TextRange)result; if (range.isEmpty()) { result = intersection; count = 1; } else if (intersection.isEmpty()) { count = 1; } else { List<TextRange> list = new ArrayList<TextRange>(); list.add(range); list.add(intersection); result = list; } } else if (intersection.isEmpty()) { count--; } else { ((List<TextRange>)result).add(intersection); } } offset += shred.getPrefix().length() + shred.getRangeInsideHost().getLength() + shred.getSuffix().length(); } return count == 0 ? Collections.<TextRange>emptyList() : count == 1 ? Collections.singletonList((TextRange)result) : (List<TextRange>)result; } @Override public boolean isInjectedFragment(final PsiFile file) { return file.getViewProvider() instanceof InjectedFileViewProvider; } @Override public PsiElement findInjectedElementAt(@NotNull PsiFile hostFile, int hostDocumentOffset) { return InjectedLanguageUtil.findInjectedElementNoCommit(hostFile, hostDocumentOffset); } @Override public void dropFileCaches(@NotNull PsiFile file) { InjectedLanguageUtil.clearCachedInjectedFragmentsForFile(file); } @Override public PsiFile getTopLevelFile(@NotNull PsiElement element) { return InjectedLanguageUtil.getTopLevelFile(element); } private final Map<Class, MultiHostInjector[]> myInjectorsClone = new HashMap<Class, MultiHostInjector[]>(); @TestOnly public static void pushInjectors(@NotNull Project project) { InjectedLanguageManagerImpl cachedManager = (InjectedLanguageManagerImpl)project.getUserData(INSTANCE_CACHE); if (cachedManager == null) return; try { assert cachedManager.myInjectorsClone.isEmpty() : cachedManager.myInjectorsClone; } finally { cachedManager.myInjectorsClone.clear(); } cachedManager.myInjectorsClone.putAll(cachedManager.getInjectorMap().getBackingMap()); } @TestOnly public static void checkInjectorsAreDisposed(@NotNull Project project) { InjectedLanguageManagerImpl cachedManager = (InjectedLanguageManagerImpl)project.getUserData(INSTANCE_CACHE); if (cachedManager == null) { return; } try { ClassMapCachingNulls<MultiHostInjector> cached = cachedManager.cachedInjectors; if (cached == null) return; for (Map.Entry<Class, MultiHostInjector[]> entry : cached.getBackingMap().entrySet()) { Class key = entry.getKey(); if (cachedManager.myInjectorsClone.isEmpty()) return; MultiHostInjector[] oldInjectors = cachedManager.myInjectorsClone.get(key); for (MultiHostInjector injector : entry.getValue()) { if (!ArrayUtil.contains(injector, oldInjectors)) { throw new AssertionError("Injector was not disposed: " + key + " -> " + injector); } } } } finally { cachedManager.myInjectorsClone.clear(); } } public interface InjProcessor { boolean process(PsiElement element, MultiHostInjector injector); } public void processInPlaceInjectorsFor(@NotNull PsiElement element, @NotNull InjProcessor processor) { MultiHostInjector[] infos = getInjectorMap().get(element.getClass()); if (infos != null) { final boolean dumb = myDumbService.isDumb(); for (MultiHostInjector injector : infos) { if (dumb && !DumbService.isDumbAware(injector)) { continue; } if (!processor.process(element, injector)) return; } } } @Override @Nullable public List<Pair<PsiElement, TextRange>> getInjectedPsiFiles(@NotNull final PsiElement host) { if (!(host instanceof PsiLanguageInjectionHost) || !((PsiLanguageInjectionHost)host).isValidHost()) { return null; } final PsiElement inTree = InjectedLanguageUtil.loadTree(host, host.getContainingFile()); final List<Pair<PsiElement, TextRange>> result = new SmartList<Pair<PsiElement, TextRange>>(); InjectedLanguageUtil.enumerate(inTree, new PsiLanguageInjectionHost.InjectedPsiVisitor() { @Override public void visit(@NotNull PsiFile injectedPsi, @NotNull List<PsiLanguageInjectionHost.Shred> places) { for (PsiLanguageInjectionHost.Shred place : places) { if (place.getHost() == inTree) { result.add(new Pair<PsiElement, TextRange>(injectedPsi, place.getRangeInsideHost())); } } } }); return result.isEmpty() ? null : result; } private static class PsiManagerRegisteredInjectorsAdapter implements MultiHostInjector { public static final PsiManagerRegisteredInjectorsAdapter INSTANCE = new PsiManagerRegisteredInjectorsAdapter(); @Override public void injectLanguages(@NotNull final MultiHostRegistrar injectionPlacesRegistrar, @NotNull PsiElement context) { final PsiLanguageInjectionHost host = (PsiLanguageInjectionHost)context; InjectedLanguagePlaces placesRegistrar = new InjectedLanguagePlaces() { @Override public void addPlace(@NotNull Language language, @NotNull TextRange rangeInsideHost, @NonNls @Nullable String prefix, @NonNls @Nullable String suffix) { ProperTextRange.assertProperRange(rangeInsideHost); injectionPlacesRegistrar.startInjecting(language).addPlace(prefix, suffix, host, rangeInsideHost) .doneInjecting(); } }; for (LanguageInjector injector : Extensions.getExtensions(LanguageInjector.EXTENSION_POINT_NAME)) { injector.getLanguagesToInject(host, placesRegistrar); } } } }