blob: 85472e245b1facb222fc9e1d078500dd68dcf515 [file] [log] [blame]
/*
* Copyright 2000-2014 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.Job;
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 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.extensions.impl.ExtensionPointImpl;
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.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.PsiDocumentManagerBase;
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 org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.util.*;
/**
* @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 volatile DaemonProgressIndicator myProgress;
public static InjectedLanguageManagerImpl getInstanceImpl(Project project) {
return (InjectedLanguageManagerImpl)InjectedLanguageManager.getInstance(project);
}
public InjectedLanguageManagerImpl(Project project, DumbService dumbService) {
myProject = project;
myDumbService = dumbService;
final ExtensionPoint<MultiHostInjector> multiPoint = Extensions.getArea(project).getExtensionPoint(MultiHostInjector.MULTIHOST_INJECTOR_EP_NAME);
((ExtensionPointImpl<MultiHostInjector>)multiPoint).addExtensionPointListener(new ExtensionPointListener<MultiHostInjector>() {
@Override
public void extensionAdded(@NotNull MultiHostInjector injector, @Nullable PluginDescriptor pluginDescriptor) {
clearInjectorCache();
}
@Override
public void extensionRemoved(@NotNull MultiHostInjector injector, @Nullable PluginDescriptor pluginDescriptor) {
clearInjectorCache();
}
}, false, this);
final ExtensionPointListener<LanguageInjector> myListener = new ExtensionPointListener<LanguageInjector>() {
@Override
public void extensionAdded(@NotNull LanguageInjector extension, @Nullable PluginDescriptor pluginDescriptor) {
clearInjectorCache();
}
@Override
public void extensionRemoved(@NotNull LanguageInjector extension, @Nullable PluginDescriptor pluginDescriptor) {
clearInjectorCache();
}
};
final ExtensionPoint<LanguageInjector> psiManagerPoint = Extensions.getRootArea().getExtensionPoint(LanguageInjector.EXTENSION_POINT_NAME);
((ExtensionPointImpl<LanguageInjector>)psiManagerPoint).addExtensionPointListener(myListener, false, this);
myProgress = new DaemonProgressIndicator();
project.getMessageBus().connect(this).subscribe(DaemonCodeAnalyzer.DAEMON_EVENT_TOPIC, new DaemonCodeAnalyzer.DaemonListenerAdapter() {
@Override
public void daemonCancelEventOccurred() {
myProgress.cancel();
}
});
}
@Override
public void dispose() {
EditorWindowImpl.disposeInvalidEditors();
}
@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
final DocumentWindow[] stillInjectedDocument = {null};
// it is here where the reparse happens and old file contents replaced
InjectedLanguageUtil.enumerate(documentWindow, hostPsiFile, new PsiLanguageInjectionHost.InjectedPsiVisitor() {
@Override
public void visit(@NotNull PsiFile injectedPsi, @NotNull List<PsiLanguageInjectionHost.Shred> places) {
stillInjectedDocument[0] = (DocumentWindow)injectedPsi.getViewProvider().getDocument();
PsiDocumentManagerBase.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, true, 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(Job.DEFAULT_PRIORITY, new Runnable() {
@Override
public void run() {
ApplicationManagerEx.getApplicationEx().tryRunReadAction(commitInjectionsRunnable);
}
});
}
}
@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) {
TextRange.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 Set<MultiHostInjector> myManualInjectors = Collections.synchronizedSet(new LinkedHashSet<MultiHostInjector>());
private volatile ClassMapCachingNulls<MultiHostInjector> cachedInjectors;
private ClassMapCachingNulls<MultiHostInjector> getInjectorMap() {
ClassMapCachingNulls<MultiHostInjector> cached = cachedInjectors;
if (cached != null) {
return cached;
}
Map<Class, MultiHostInjector[]> injectors = ContainerUtil.newHashMap();
List<MultiHostInjector> allInjectors = ContainerUtil.newArrayList();
allInjectors.addAll(myManualInjectors);
Collections.addAll(allInjectors, MultiHostInjector.MULTIHOST_INJECTOR_EP_NAME.getExtensions(myProject));
if (LanguageInjector.EXTENSION_POINT_NAME.getExtensions().length > 0) {
allInjectors.add(PsiManagerRegisteredInjectorsAdapter.INSTANCE);
}
for (MultiHostInjector injector : allInjectors) {
for (Class<? extends PsiElement> place : injector.elementsToInjectIn()) {
LOG.assertTrue(place != null, injector);
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) {
myManualInjectors.add(injector);
clearInjectorCache();
}
@Override
public boolean unregisterMultiHostInjector(@NotNull MultiHostInjector injector) {
try {
return myManualInjectors.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(@NotNull 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);
}
@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);
}
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 getLanguagesToInject(@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) {
TextRange.assertProperRange(rangeInsideHost);
injectionPlacesRegistrar
.startInjecting(language)
.addPlace(prefix, suffix, host, rangeInsideHost)
.doneInjecting();
}
};
for (LanguageInjector injector : Extensions.getExtensions(LanguageInjector.EXTENSION_POINT_NAME)) {
injector.getLanguagesToInject(host, placesRegistrar);
}
}
@Override
@NotNull
public List<? extends Class<? extends PsiElement>> elementsToInjectIn() {
return Arrays.asList(PsiLanguageInjectionHost.class);
}
}
}