/*
* Copyright 2000-2016 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;
import com.google.common.util.concurrent.Atomics;
import com.intellij.injected.editor.DocumentWindow;
import com.intellij.lang.*;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.command.undo.UndoConstants;
import com.intellij.openapi.diagnostic.Attachment;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.impl.LoadTextUtil;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeRegistry;
import com.intellij.openapi.fileTypes.PlainTextLanguage;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.FileIndexFacade;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.openapi.vfs.NonPhysicalFileSystem;
import com.intellij.openapi.vfs.PersistentFSConstants;
import com.intellij.openapi.vfs.VFileProperty;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.impl.*;
import com.intellij.psi.impl.file.PsiBinaryFileImpl;
import com.intellij.psi.impl.file.PsiLargeBinaryFileImpl;
import com.intellij.psi.impl.file.PsiLargeTextFileImpl;
import com.intellij.psi.impl.file.impl.FileManager;
import com.intellij.psi.impl.source.PsiFileImpl;
import com.intellij.psi.impl.source.PsiPlainTextFileImpl;
import com.intellij.psi.impl.source.SourceTreeToPsiMap;
import com.intellij.psi.impl.source.tree.FileElement;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.util.LocalTimeCounter;
import com.intellij.util.ReflectionUtil;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
public class SingleRootFileViewProvider extends UserDataHolderBase implements FileViewProvider {
private static final Key<Boolean> OUR_NO_SIZE_LIMIT_KEY = Key.create("no.size.limit");
private static final Logger LOG = Logger.getInstance("#" + SingleRootFileViewProvider.class.getCanonicalName());
public static final Key<Object> FREE_THREADED = Key.create("FREE_THREADED");
@NotNull
private final PsiManager myManager;
@NotNull
private final VirtualFile myVirtualFile;
private final boolean myEventSystemEnabled;
private final boolean myPhysical;
private final AtomicReference<PsiFile> myPsiFile = Atomics.newReference();
private volatile Content myContent;
private volatile Reference<Document> myDocument;
@NotNull
private final Language myBaseLanguage;
@NotNull
private final FileType myFileType;
public SingleRootFileViewProvider(@NotNull PsiManager manager, @NotNull VirtualFile file) {
this(manager, file, true);
}
public SingleRootFileViewProvider(@NotNull PsiManager manager, @NotNull VirtualFile virtualFile, final boolean eventSystemEnabled) {
this(manager, virtualFile, eventSystemEnabled, virtualFile.getFileType());
}
public SingleRootFileViewProvider(@NotNull PsiManager manager,
@NotNull VirtualFile virtualFile,
final boolean eventSystemEnabled,
@NotNull final FileType fileType) {
this(manager, virtualFile, eventSystemEnabled, calcBaseLanguage(virtualFile, manager.getProject(), fileType), fileType);
}
protected SingleRootFileViewProvider(@NotNull PsiManager manager,
@NotNull VirtualFile virtualFile,
final boolean eventSystemEnabled,
@NotNull Language language) {
this(manager, virtualFile, eventSystemEnabled, language, virtualFile.getFileType());
}
protected SingleRootFileViewProvider(@NotNull PsiManager manager,
@NotNull VirtualFile virtualFile,
final boolean eventSystemEnabled,
@NotNull Language language,
@NotNull FileType type) {
myManager = manager;
myVirtualFile = virtualFile;
myEventSystemEnabled = eventSystemEnabled;
myBaseLanguage = language;
setContent(new VirtualFileContent());
myPhysical = isEventSystemEnabled() && !(virtualFile instanceof LightVirtualFile) && !(virtualFile.getFileSystem() instanceof NonPhysicalFileSystem);
virtualFile.putUserData(FREE_THREADED, isFreeThreaded(this));
myFileType = type;
}
public static boolean isFreeThreaded(@NotNull FileViewProvider provider) {
return provider.getVirtualFile() instanceof LightVirtualFile && !provider.isEventSystemEnabled();
}
@Override
@NotNull
public Language getBaseLanguage() {
return myBaseLanguage;
}
private static Language calcBaseLanguage(@NotNull VirtualFile file, @NotNull Project project, @NotNull final FileType fileType) {
if (fileType.isBinary()) return Language.ANY;
if (isTooLargeForIntelligence(file)) return PlainTextLanguage.INSTANCE;
Language language = LanguageUtil.getLanguageForPsi(project, file);
return language != null ? language : PlainTextLanguage.INSTANCE;
}
@Override
@NotNull
public Set<Language> getLanguages() {
return Collections.singleton(getBaseLanguage());
}
@Override
@Nullable
public final PsiFile getPsi(@NotNull Language target) {
if (!isPhysical()) {
FileManager fileManager = ((PsiManagerEx)myManager).getFileManager();
VirtualFile virtualFile = getVirtualFile();
if (fileManager.findCachedViewProvider(virtualFile) == null) {
fileManager.setViewProvider(virtualFile, this);
}
}
return getPsiInner(target);
}
@Override
@NotNull
public List<PsiFile> getAllFiles() {
return ContainerUtil.createMaybeSingletonList(getPsi(getBaseLanguage()));
}
@Nullable
protected PsiFile getPsiInner(@NotNull Language target) {
if (target != getBaseLanguage()) {
return null;
}
PsiFile psiFile = myPsiFile.get();
if (psiFile == null) {
psiFile = createFile();
if (psiFile == null) {
psiFile = PsiUtilCore.NULL_PSI_FILE;
}
boolean set = myPsiFile.compareAndSet(null, psiFile);
if (!set && psiFile != PsiUtilCore.NULL_PSI_FILE) {
PsiFile alreadyCreated = myPsiFile.get();
if (alreadyCreated == psiFile) {
LOG.error(this + ".createFile() must create new file instance but got the same: " + psiFile);
}
if (psiFile instanceof PsiFileEx) {
DebugUtil.startPsiModification("invalidating throw-away copy");
try {
((PsiFileEx)psiFile).markInvalidated();
}
finally {
DebugUtil.finishPsiModification();
}
}
psiFile = alreadyCreated;
}
}
return psiFile == PsiUtilCore.NULL_PSI_FILE ? null : psiFile;
}
@Override
public void beforeContentsSynchronized() {
}
@Override
public void contentsSynchronized() {
if (myContent instanceof PsiFileContent) {
setContent(new VirtualFileContent());
}
checkLengthConsistency();
}
public void beforeDocumentChanged(@Nullable PsiFile psiCause) {
PsiFile psiFile = psiCause != null ? psiCause : getPsi(getBaseLanguage());
if (psiFile instanceof PsiFileImpl && myContent instanceof VirtualFileContent) {
setContent(new PsiFileContent((PsiFileImpl)psiFile, psiCause == null ? getModificationStamp() : LocalTimeCounter.currentTime()));
checkLengthConsistency();
}
}
public final void onContentReload() {
List<PsiFile> files = getCachedPsiFiles();
List<PsiTreeChangeEventImpl> events = ContainerUtil.newArrayList();
List<PsiTreeChangeEventImpl> genericEvents = ContainerUtil.newArrayList();
for (PsiFile file : files) {
genericEvents.add(createChildrenChangeEvent(file, true));
events.add(createChildrenChangeEvent(file, false));
}
beforeContentsSynchronized();
for (PsiTreeChangeEventImpl event : genericEvents) {
((PsiManagerImpl)getManager()).beforeChildrenChange(event);
}
for (PsiTreeChangeEventImpl event : events) {
((PsiManagerImpl)getManager()).beforeChildrenChange(event);
}
for (PsiFile psiFile : files) {
if (psiFile instanceof PsiFileEx) {
((PsiFileEx)psiFile).onContentReload();
}
}
for (PsiTreeChangeEventImpl event : events) {
((PsiManagerImpl)getManager()).childrenChanged(event);
}
for (PsiTreeChangeEventImpl event : genericEvents) {
((PsiManagerImpl)getManager()).childrenChanged(event);
}
contentsSynchronized();
}
private PsiTreeChangeEventImpl createChildrenChangeEvent(PsiFile file, boolean generic) {
PsiTreeChangeEventImpl event = new PsiTreeChangeEventImpl(myManager);
event.setParent(file);
event.setFile(file);
event.setGenericChange(generic);
if (file instanceof PsiFileImpl && ((PsiFileImpl)file).isContentsLoaded()) {
event.setOffset(0);
event.setOldLength(file.getTextLength());
}
return event;
}
@Override
public void rootChanged(@NotNull PsiFile psiFile) {
if (psiFile instanceof PsiFileImpl && ((PsiFileImpl)psiFile).isContentsLoaded()) {
setContent(new PsiFileContent((PsiFileImpl)psiFile, LocalTimeCounter.currentTime()));
}
}
@Override
public boolean isEventSystemEnabled() {
return myEventSystemEnabled;
}
@Override
public boolean isPhysical() {
return myPhysical;
}
@Override
public long getModificationStamp() {
return getContent().getModificationStamp();
}
@Override
public boolean supportsIncrementalReparse(@NotNull final Language rootLanguage) {
return true;
}
public PsiFile getCachedPsi(@NotNull Language target) {
PsiFile file = myPsiFile.get();
return file == PsiUtilCore.NULL_PSI_FILE ? null : file;
}
public List<PsiFile> getCachedPsiFiles() {
return ContainerUtil.createMaybeSingletonList(getCachedPsi(myBaseLanguage));
}
@NotNull
public List<FileElement> getKnownTreeRoots() {
PsiFile psiFile = getCachedPsi(myBaseLanguage);
if (!(psiFile instanceof PsiFileImpl)) return Collections.emptyList();
FileElement element = ((PsiFileImpl)psiFile).getTreeElement();
return ContainerUtil.createMaybeSingletonList(element);
}
private PsiFile createFile() {
try {
final VirtualFile vFile = getVirtualFile();
if (vFile.isDirectory()) return null;
if (isIgnored()) return null;
final Project project = myManager.getProject();
if (isPhysical() && vFile.isInLocalFileSystem()) { // check directories consistency
final VirtualFile parent = vFile.getParent();
if (parent == null) return null;
final PsiDirectory psiDir = getManager().findDirectory(parent);
if (psiDir == null) {
FileIndexFacade indexFacade = FileIndexFacade.getInstance(project);
if (!indexFacade.isInLibrarySource(vFile) && !indexFacade.isInLibraryClasses(vFile)) {
return null;
}
}
}
return createFile(project, vFile, myFileType);
}
catch (ProcessCanceledException e) {
throw e;
}
catch (Throwable e) {
LOG.error(e);
return null;
}
}
protected boolean isIgnored() {
final VirtualFile file = getVirtualFile();
return !(file instanceof LightVirtualFile) && FileTypeRegistry.getInstance().isFileIgnored(file);
}
@Nullable
protected PsiFile createFile(@NotNull Project project, @NotNull VirtualFile file, @NotNull FileType fileType) {
if (fileType.isBinary() || file.is(VFileProperty.SPECIAL)) {
return isTooLargeForContentLoading(file)
? new PsiLargeBinaryFileImpl(((PsiManagerImpl)getManager()), this)
: new PsiBinaryFileImpl((PsiManagerImpl)getManager(), this);
}
if (!isTooLargeForIntelligence(file)) {
final PsiFile psiFile = createFile(getBaseLanguage());
if (psiFile != null) return psiFile;
}
if (isTooLargeForContentLoading(file)) {
return new PsiLargeTextFileImpl(this);
}
return new PsiPlainTextFileImpl(this);
}
@Deprecated
public static boolean isTooLarge(@NotNull VirtualFile vFile) {
return isTooLargeForIntelligence(vFile);
}
public static boolean isTooLargeForIntelligence(@NotNull VirtualFile vFile) {
if (!checkFileSizeLimit(vFile)) return false;
return fileSizeIsGreaterThan(vFile, PersistentFSConstants.getMaxIntellisenseFileSize());
}
public static boolean isTooLargeForContentLoading(@NotNull VirtualFile vFile) {
return fileSizeIsGreaterThan(vFile, PersistentFSConstants.FILE_LENGTH_TO_CACHE_THRESHOLD);
}
private static boolean checkFileSizeLimit(@NotNull VirtualFile vFile) {
return !Boolean.TRUE.equals(vFile.getUserData(OUR_NO_SIZE_LIMIT_KEY));
}
public static void doNotCheckFileSizeLimit(@NotNull VirtualFile vFile) {
vFile.putUserData(OUR_NO_SIZE_LIMIT_KEY, Boolean.TRUE);
}
public static boolean isTooLargeForIntelligence(@NotNull VirtualFile vFile, final long contentSize) {
if (!checkFileSizeLimit(vFile)) return false;
return contentSize > PersistentFSConstants.getMaxIntellisenseFileSize();
}
@SuppressWarnings("UnusedParameters")
public static boolean isTooLargeForContentLoading(@NotNull VirtualFile vFile, final long contentSize) {
return contentSize > PersistentFSConstants.FILE_LENGTH_TO_CACHE_THRESHOLD;
}
public static boolean fileSizeIsGreaterThan(@NotNull VirtualFile vFile, final long maxBytes) {
if (vFile instanceof LightVirtualFile) {
// This is optimization in order to avoid conversion of [large] file contents to bytes
final int lengthInChars = ((LightVirtualFile)vFile).getContent().length();
if (lengthInChars < maxBytes / 2) return false;
if (lengthInChars > maxBytes) return true;
}
return vFile.getLength() > maxBytes;
}
@Nullable
protected PsiFile createFile(@NotNull Language lang) {
if (lang != getBaseLanguage()) return null;
final ParserDefinition parserDefinition = LanguageParserDefinitions.INSTANCE.forLanguage(lang);
if (parserDefinition != null) {
return parserDefinition.createFile(this);
}
return null;
}
@Override
@NotNull
public PsiManager getManager() {
return myManager;
}
@Override
@NotNull
public CharSequence getContents() {
return getContent().getText();
}
@Override
@NotNull
public VirtualFile getVirtualFile() {
return myVirtualFile;
}
@Nullable
private Document getCachedDocument() {
final Document document = com.intellij.reference.SoftReference.dereference(myDocument);
if (document != null) return document;
return FileDocumentManager.getInstance().getCachedDocument(getVirtualFile());
}
@Override
public Document getDocument() {
Document document = com.intellij.reference.SoftReference.dereference(myDocument);
if (document == null/* TODO[ik] make this change && isEventSystemEnabled()*/) {
document = FileDocumentManager.getInstance().getDocument(getVirtualFile());
myDocument = document == null ? null : new SoftReference<>(document);
}
return document;
}
@SuppressWarnings("MethodDoesntCallSuperMethod")
@Override
public FileViewProvider clone() {
final VirtualFile origFile = getVirtualFile();
LightVirtualFile copy = new LightVirtualFile(origFile.getName(), myFileType, getContents(), origFile.getCharset(), getModificationStamp());
copy.setOriginalFile(origFile);
copy.putUserData(UndoConstants.DONT_RECORD_UNDO, Boolean.TRUE);
copy.setCharset(origFile.getCharset());
return createCopy(copy);
}
@NotNull
@Override
public SingleRootFileViewProvider createCopy(@NotNull final VirtualFile copy) {
return new SingleRootFileViewProvider(getManager(), copy, false, myBaseLanguage);
}
@Override
public PsiReference findReferenceAt(final int offset) {
final PsiFile psiFile = getPsi(getBaseLanguage());
return findReferenceAt(psiFile, offset);
}
@Override
public PsiElement findElementAt(final int offset, @NotNull final Language language) {
final PsiFile psiFile = getPsi(language);
return psiFile != null ? findElementAt(psiFile, offset) : null;
}
@Override
@Nullable
public PsiReference findReferenceAt(final int offset, @NotNull final Language language) {
final PsiFile psiFile = getPsi(language);
return psiFile != null ? findReferenceAt(psiFile, offset) : null;
}
@Nullable
protected static PsiReference findReferenceAt(@Nullable final PsiFile psiFile, final int offset) {
if (psiFile == null) return null;
int offsetInElement = offset;
PsiElement child = psiFile.getFirstChild();
while (child != null) {
final int length = child.getTextLength();
if (length <= offsetInElement) {
offsetInElement -= length;
child = child.getNextSibling();
continue;
}
return child.findReferenceAt(offsetInElement);
}
return null;
}
@Override
public PsiElement findElementAt(final int offset) {
return findElementAt(getPsi(getBaseLanguage()), offset);
}
@Override
public PsiElement findElementAt(int offset, @NotNull Class<? extends Language> lang) {
if (!ReflectionUtil.isAssignable(lang, getBaseLanguage().getClass())) return null;
return findElementAt(offset);
}
@Nullable
public static PsiElement findElementAt(@Nullable PsiElement psiFile, final int offset) {
ASTNode node = psiFile == null ? null : psiFile.getNode();
return node == null ? null : SourceTreeToPsiMap.treeElementToPsi(node.findLeafElementAt(offset));
}
public void forceCachedPsi(@NotNull PsiFile psiFile) {
PsiFile prev = myPsiFile.getAndSet(psiFile);
if (prev != null && prev != psiFile && prev instanceof PsiFileEx) {
((PsiFileEx)prev).markInvalidated();
}
((PsiManagerEx)myManager).getFileManager().setViewProvider(getVirtualFile(), this);
}
@NotNull
private Content getContent() {
return myContent;
}
private void setContent(@NotNull Content content) {
myContent = content;
}
private void checkLengthConsistency() {
Document document = getCachedDocument();
if (document instanceof DocumentWindow) {
return;
}
if (document != null && ((PsiDocumentManagerBase)PsiDocumentManager.getInstance(myManager.getProject())).getSynchronizer().isInSynchronization(document)) {
return;
}
List<FileElement> knownTreeRoots = getKnownTreeRoots();
if (knownTreeRoots.isEmpty()) return;
int fileLength = myContent.getTextLength();
for (FileElement fileElement : knownTreeRoots) {
int nodeLength = fileElement.getTextLength();
if (nodeLength != fileLength) {
PsiUtilCore.ensureValid(fileElement.getPsi());
List<Attachment> attachments = ContainerUtil.newArrayList(new Attachment(myVirtualFile.getNameWithoutExtension() + ".tree.txt", fileElement.getText()),
new Attachment(myVirtualFile.getNameWithoutExtension() + ".file.txt", myContent.toString()));
if (document != null) {
attachments.add(new Attachment(myVirtualFile.getNameWithoutExtension() + ".document.txt", document.getText()));
}
// exceptions here should be assigned to peter
LOG.error("Inconsistent " + fileElement.getElementType() + " tree in " + this + "; nodeLength=" + nodeLength + "; fileLength=" + fileLength,
attachments.toArray(Attachment.EMPTY_ARRAY));
}
}
}
@NonNls
@Override
public String toString() {
return getClass().getSimpleName() + "{myVirtualFile=" + myVirtualFile + ", content=" + getContent() + '}';
}
public void markInvalidated() {
PsiFile psiFile = getCachedPsi(myBaseLanguage);
if (psiFile instanceof PsiFileEx) {
((PsiFileEx)psiFile).markInvalidated();
}
}
private interface Content {
CharSequence getText();
int getTextLength();
long getModificationStamp();
}
private class VirtualFileContent implements Content {
@Override
public CharSequence getText() {
final VirtualFile virtualFile = getVirtualFile();
if (virtualFile instanceof LightVirtualFile) {
Document doc = getCachedDocument();
if (doc != null) return getLastCommittedText(doc);
return ((LightVirtualFile)virtualFile).getContent();
}
final Document document = getDocument();
if (document == null) {
return LoadTextUtil.loadText(virtualFile);
}
return getLastCommittedText(document);
}
@Override
public int getTextLength() {
return getText().length();
}
@Override
public long getModificationStamp() {
final Document document = getCachedDocument();
if (document == null) {
return getVirtualFile().getModificationStamp();
}
return getLastCommittedStamp(document);
}
@NonNls
@Override
public String toString() {
return "VirtualFileContent{size=" + getVirtualFile().getLength() + "}";
}
}
private CharSequence getLastCommittedText(Document document) {
return PsiDocumentManager.getInstance(myManager.getProject()).getLastCommittedText(document);
}
private long getLastCommittedStamp(Document document) {
return PsiDocumentManager.getInstance(myManager.getProject()).getLastCommittedStamp(document);
}
private class PsiFileContent implements Content {
private final PsiFileImpl myFile;
private volatile String myContent;
private final long myModificationStamp;
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
private final List<FileElement> myFileElementHardRefs = new SmartList<>();
private PsiFileContent(final PsiFileImpl file, final long modificationStamp) {
myFile = file;
myModificationStamp = modificationStamp;
for (PsiFile aFile : getAllFiles()) {
if (aFile instanceof PsiFileImpl) {
myFileElementHardRefs.add(((PsiFileImpl)aFile).calcTreeElement());
}
}
}
@Override
public CharSequence getText() {
String content = myContent;
if (content == null) {
myContent = content = ReadAction.compute(() -> myFile.calcTreeElement().getText());
}
return content;
}
@Override
public int getTextLength() {
String content = myContent;
if (content != null) {
return content.length();
}
return myFile.calcTreeElement().getTextLength();
}
@Override
public long getModificationStamp() {
return myModificationStamp;
}
}
@NotNull
@Override
public PsiFile getStubBindingRoot() {
final PsiFile psi = getPsi(getBaseLanguage());
assert psi != null;
return psi;
}
@NotNull
@Override
public final FileType getFileType() {
return myFileType;
}
}