/* * 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.file.impl; import com.intellij.AppTopics; import com.intellij.ProjectTopics; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileEditor.FileDocumentManagerAdapter; import com.intellij.openapi.fileEditor.impl.FileDocumentManagerImpl; import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.fileTypes.FileTypeEvent; import com.intellij.openapi.fileTypes.FileTypeListener; import com.intellij.openapi.fileTypes.FileTypeManager; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.*; import com.intellij.openapi.roots.impl.PushedFilePropertiesUpdater; import com.intellij.openapi.startup.StartupManager; import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.vfs.*; import com.intellij.openapi.vfs.impl.BulkVirtualFileListenerAdapter; import com.intellij.psi.*; import com.intellij.psi.impl.PsiManagerImpl; import com.intellij.psi.impl.PsiTreeChangeEventImpl; import com.intellij.psi.impl.smartPointers.SmartPointerManagerImpl; import com.intellij.util.FileContentUtilCore; import com.intellij.util.messages.MessageBusConnection; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public class PsiVFSListener extends VirtualFileAdapter { private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.file.impl.PsiVFSListener"); private final FileTypeManager myFileTypeManager; private final ProjectRootManager myProjectRootManager; private final PsiManagerImpl myManager; private final FileManagerImpl myFileManager; private final MessageBusConnection myConnection; private final Project myProject; public PsiVFSListener(Project project) { myProject = project; myFileTypeManager = FileTypeManager.getInstance(); myProjectRootManager = ProjectRootManager.getInstance(project); myManager = (PsiManagerImpl) PsiManager.getInstance(project); myFileManager = (FileManagerImpl) myManager.getFileManager(); myConnection = project.getMessageBus().connect(project); StartupManager.getInstance(project).registerPreStartupActivity(new Runnable() { @Override public void run() { myConnection.subscribe(VirtualFileManager.VFS_CHANGES, new BulkVirtualFileListenerAdapter(PsiVFSListener.this)); myConnection.subscribe(ProjectTopics.PROJECT_ROOTS, new MyModuleRootListener()); myConnection.subscribe(FileTypeManager.TOPIC, new FileTypeListener.Adapter() { @Override public void fileTypesChanged(@NotNull FileTypeEvent e) { myFileManager.processFileTypesChanged(); } }); myConnection.subscribe(AppTopics.FILE_DOCUMENT_SYNC, new MyFileDocumentManagerAdapter()); myFileManager.markInitialized(); } }); } @Nullable private PsiDirectory getCachedDirectory(VirtualFile parent) { return parent == null ? null : myFileManager.getCachedDirectory(parent); } @Override public void fileCreated(@NotNull VirtualFileEvent event) { final VirtualFile vFile = event.getFile(); ApplicationManager.getApplication().runWriteAction( new ExternalChangeAction() { @Override public void run() { VirtualFile parent = vFile.getParent(); PsiDirectory parentDir = getCachedDirectory(parent); if (parentDir == null) return; // do not notifyListeners event if parent directory was never accessed via PSI if (!vFile.isDirectory()) { PsiFile psiFile = myFileManager.findFile(vFile); if (psiFile != null && psiFile.getProject() == myManager.getProject()) { PsiTreeChangeEventImpl treeEvent = new PsiTreeChangeEventImpl(myManager); treeEvent.setParent(parentDir); myManager.beforeChildAddition(treeEvent); treeEvent.setChild(psiFile); myManager.childAdded(treeEvent); } } else { PsiDirectory psiDir = myFileManager.findDirectory(vFile); if (psiDir != null && psiDir.getProject() == myManager.getProject()) { PsiTreeChangeEventImpl treeEvent = new PsiTreeChangeEventImpl(myManager); treeEvent.setParent(parentDir); myManager.beforeChildAddition(treeEvent); treeEvent.setChild(psiDir); myManager.childAdded(treeEvent); } } } } ); } @Override public void beforeFileDeletion(@NotNull VirtualFileEvent event) { final VirtualFile vFile = event.getFile(); VirtualFile parent = vFile.getParent(); final PsiDirectory parentDir = getCachedDirectory(parent); if (parentDir == null) return; // do not notify listeners if parent directory was never accessed via PSI ApplicationManager.getApplication().runWriteAction( new ExternalChangeAction() { @Override public void run() { if (!vFile.isDirectory()) { PsiFile psiFile = myFileManager.getCachedPsiFile(vFile); if (psiFile != null) { PsiTreeChangeEventImpl treeEvent = new PsiTreeChangeEventImpl(myManager); treeEvent.setParent(parentDir); treeEvent.setChild(psiFile); myManager.beforeChildRemoval(treeEvent); } } else { PsiDirectory psiDir = myFileManager.findDirectory(vFile); if (psiDir != null) { PsiTreeChangeEventImpl treeEvent = new PsiTreeChangeEventImpl(myManager); treeEvent.setParent(parentDir); treeEvent.setChild(psiDir); myManager.beforeChildRemoval(treeEvent); } } } } ); } @Override public void fileDeleted(@NotNull final VirtualFileEvent event) { final VirtualFile vFile = event.getFile(); VirtualFile parent = event.getParent(); final PsiDirectory parentDir = getCachedDirectory(parent); final PsiFile psiFile = myFileManager.getCachedPsiFileInner(vFile); if (psiFile != null) { myFileManager.setViewProvider(vFile, null); if (parentDir != null) { ApplicationManager.getApplication().runWriteAction(new ExternalChangeAction() { @Override public void run() { PsiTreeChangeEventImpl treeEvent = new PsiTreeChangeEventImpl(myManager); treeEvent.setParent(parentDir); treeEvent.setChild(psiFile); myManager.childRemoved(treeEvent); } }); } } else { final PsiDirectory psiDir = myFileManager.getCachedDirectory(vFile); if (psiDir != null) { myFileManager.removeInvalidFilesAndDirs(false); if (parentDir != null) { ApplicationManager.getApplication().runWriteAction(new ExternalChangeAction() { @Override public void run() { PsiTreeChangeEventImpl treeEvent = new PsiTreeChangeEventImpl(myManager); treeEvent.setParent(parentDir); treeEvent.setChild(psiDir); myManager.childRemoved(treeEvent); } }); } } } } @Override public void beforePropertyChange(@NotNull final VirtualFilePropertyEvent event) { final VirtualFile vFile = event.getFile(); final String propertyName = event.getPropertyName(); VirtualFile parent = vFile.getParent(); final PsiDirectory parentDir = getCachedDirectory(parent); if (parent != null && parentDir == null) return; // do not notifyListeners event if parent directory was never accessed via PSI ApplicationManager.getApplication().runWriteAction( new ExternalChangeAction() { @Override public void run() { PsiTreeChangeEventImpl treeEvent = new PsiTreeChangeEventImpl(myManager); treeEvent.setParent(parentDir); if (VirtualFile.PROP_NAME.equals(propertyName)) { final String newName = (String)event.getNewValue(); if (parentDir == null) return; if (vFile.isDirectory()) { PsiDirectory psiDir = myFileManager.findDirectory(vFile); if (psiDir != null) { if (!myFileTypeManager.isFileIgnored(newName)) { treeEvent.setChild(psiDir); treeEvent.setPropertyName(PsiTreeChangeEvent.PROP_DIRECTORY_NAME); treeEvent.setOldValue(vFile.getName()); treeEvent.setNewValue(newName); myManager.beforePropertyChange(treeEvent); } else { treeEvent.setChild(psiDir); myManager.beforeChildRemoval(treeEvent); } } else { if ((!Registry.is("ide.hide.excluded.files") || !isExcludeRoot(vFile)) && !myFileTypeManager.isFileIgnored(newName)) { myManager.beforeChildAddition(treeEvent); } } } else { final FileViewProvider viewProvider = myFileManager.findViewProvider(vFile); PsiFile psiFile = viewProvider.getPsi(viewProvider.getBaseLanguage()); PsiFile psiFile1 = createFileCopyWithNewName(vFile, newName); if (psiFile != null) { if (psiFile1 == null) { treeEvent.setChild(psiFile); myManager.beforeChildRemoval(treeEvent); } else if (!psiFile1.getClass().equals(psiFile.getClass())) { treeEvent.setOldChild(psiFile); myManager.beforeChildReplacement(treeEvent); } else { treeEvent.setChild(psiFile); treeEvent.setPropertyName(PsiTreeChangeEvent.PROP_FILE_NAME); treeEvent.setOldValue(vFile.getName()); treeEvent.setNewValue(newName); myManager.beforePropertyChange(treeEvent); } } else { if (psiFile1 != null) { myManager.beforeChildAddition(treeEvent); } } } } else if (VirtualFile.PROP_WRITABLE.equals(propertyName)) { PsiFile psiFile = myFileManager.getCachedPsiFileInner(vFile); if (psiFile == null) return; treeEvent.setElement(psiFile); treeEvent.setPropertyName(PsiTreeChangeEvent.PROP_WRITABLE); treeEvent.setOldValue(event.getOldValue()); treeEvent.setNewValue(event.getNewValue()); myManager.beforePropertyChange(treeEvent); } } } ); } private boolean isExcludeRoot(VirtualFile file) { VirtualFile parent = file.getParent(); if (parent == null) return false; Module module = myProjectRootManager.getFileIndex().getModuleForFile(parent); if (module == null) return false; VirtualFile[] excludeRoots = ModuleRootManager.getInstance(module).getExcludeRoots(); for (VirtualFile root : excludeRoots) { if (root.equals(file)) return true; } return false; } @Override public void propertyChanged(@NotNull final VirtualFilePropertyEvent event) { final String propertyName = event.getPropertyName(); final VirtualFile vFile = event.getFile(); final FileViewProvider oldFileViewProvider = myFileManager.findCachedViewProvider(vFile); final PsiFile oldPsiFile; if (oldFileViewProvider instanceof SingleRootFileViewProvider) { oldPsiFile = ((SingleRootFileViewProvider)oldFileViewProvider).getCachedPsi(oldFileViewProvider.getBaseLanguage()); } else { oldPsiFile = null; } VirtualFile parent = vFile.getParent(); final PsiDirectory parentDir = getCachedDirectory(parent); if (oldFileViewProvider != null // there is no need to rebuild if there were no PSI in the first place && FileContentUtilCore.FORCE_RELOAD_REQUESTOR.equals(event.getRequestor())) { myFileManager.forceReload(vFile); return; } // do not suppress reparse request for light files if (parentDir == null) { boolean fire = VirtualFile.PROP_NAME.equals(propertyName) && vFile.isDirectory(); if (fire) { PsiDirectory psiDir = myFileManager.getCachedDirectory(vFile); fire = psiDir != null; } if (!fire) return; // do not fire event if parent directory was never accessed via PSI } if (oldPsiFile != null && oldPsiFile.isPhysical()) { ((SmartPointerManagerImpl)SmartPointerManager.getInstance(myManager.getProject())).fastenBelts(oldPsiFile, 0, null); } ApplicationManager.getApplication().runWriteAction( new ExternalChangeAction() { @Override public void run() { PsiTreeChangeEventImpl treeEvent = new PsiTreeChangeEventImpl(myManager); treeEvent.setParent(parentDir); if (VirtualFile.PROP_NAME.equals(propertyName)) { if (vFile.isDirectory()) { PsiDirectory psiDir = myFileManager.getCachedDirectory(vFile); if (psiDir != null) { if (myFileTypeManager.isFileIgnored(vFile)) { myFileManager.removeFilesAndDirsRecursively(vFile); treeEvent.setChild(psiDir); myManager.childRemoved(treeEvent); } else { treeEvent.setElement(psiDir); treeEvent.setPropertyName(PsiTreeChangeEvent.PROP_DIRECTORY_NAME); treeEvent.setOldValue(event.getOldValue()); treeEvent.setNewValue(event.getNewValue()); myManager.propertyChanged(treeEvent); } } else { PsiDirectory psiDir1 = myFileManager.findDirectory(vFile); if (psiDir1 != null) { treeEvent.setChild(psiDir1); myManager.childAdded(treeEvent); } } } else { final FileViewProvider fileViewProvider = myFileManager.createFileViewProvider(vFile, true); final PsiFile newPsiFile = fileViewProvider.getPsi(fileViewProvider.getBaseLanguage()); if(oldPsiFile != null) { if (newPsiFile == null) { myFileManager.setViewProvider(vFile, null); treeEvent.setChild(oldPsiFile); myManager.childRemoved(treeEvent); } else if (!newPsiFile.getClass().equals(oldPsiFile.getClass()) || newPsiFile.getFileType() != myFileTypeManager.getFileTypeByFileName((String)event.getOldValue()) || languageDialectChanged(newPsiFile, (String)event.getOldValue()) || !oldFileViewProvider.getLanguages().equals(fileViewProvider.getLanguages())) { myFileManager.setViewProvider(vFile, fileViewProvider); treeEvent.setOldChild(oldPsiFile); treeEvent.setNewChild(newPsiFile); myManager.childReplaced(treeEvent); } else { treeEvent.setElement(oldPsiFile); treeEvent.setPropertyName(PsiTreeChangeEvent.PROP_FILE_NAME); treeEvent.setOldValue(event.getOldValue()); treeEvent.setNewValue(event.getNewValue()); myManager.propertyChanged(treeEvent); } } else if (newPsiFile != null) { myFileManager.setViewProvider(vFile, fileViewProvider); if (parentDir != null) { treeEvent.setChild(newPsiFile); myManager.childAdded(treeEvent); } } } } else if (VirtualFile.PROP_WRITABLE.equals(propertyName)) { if (oldPsiFile == null) return; treeEvent.setElement(oldPsiFile); treeEvent.setPropertyName(PsiTreeChangeEvent.PROP_WRITABLE); treeEvent.setOldValue(event.getOldValue()); treeEvent.setNewValue(event.getNewValue()); myManager.propertyChanged(treeEvent); } else if (VirtualFile.PROP_ENCODING.equals(propertyName)) { if (oldPsiFile == null) return; treeEvent.setElement(oldPsiFile); treeEvent.setPropertyName(VirtualFile.PROP_ENCODING); treeEvent.setOldValue(event.getOldValue()); treeEvent.setNewValue(event.getNewValue()); myManager.propertyChanged(treeEvent); } } } ); } @Override public void beforeFileMovement(@NotNull VirtualFileMoveEvent event) { final VirtualFile vFile = event.getFile(); final PsiDirectory oldParentDir = myFileManager.findDirectory(event.getOldParent()); final PsiDirectory newParentDir = myFileManager.findDirectory(event.getNewParent()); if (oldParentDir == null && newParentDir == null) return; if (myFileTypeManager.isFileIgnored(vFile)) return; ApplicationManager.getApplication().runWriteAction( new ExternalChangeAction() { @Override public void run() { PsiTreeChangeEventImpl treeEvent = new PsiTreeChangeEventImpl(myManager); boolean isExcluded = vFile.isDirectory() && Registry.is("ide.hide.excluded.files") && myProjectRootManager.getFileIndex().isExcluded(vFile); if (oldParentDir != null && !isExcluded) { if (newParentDir != null) { treeEvent.setOldParent(oldParentDir); treeEvent.setNewParent(newParentDir); if (vFile.isDirectory()) { PsiDirectory psiDir = myFileManager.findDirectory(vFile); treeEvent.setChild(psiDir); } else { PsiFile psiFile = myFileManager.findFile(vFile); treeEvent.setChild(psiFile); } myManager.beforeChildMovement(treeEvent); } else { treeEvent.setParent(oldParentDir); if (vFile.isDirectory()) { PsiDirectory psiDir = myFileManager.findDirectory(vFile); treeEvent.setChild(psiDir); } else { PsiFile psiFile = myFileManager.findFile(vFile); treeEvent.setChild(psiFile); } myManager.beforeChildRemoval(treeEvent); } } else { LOG.assertTrue(newParentDir != null); // checked above treeEvent.setParent(newParentDir); myManager.beforeChildAddition(treeEvent); } } } ); } @Override public void fileMoved(@NotNull VirtualFileMoveEvent event) { // let PushedFilePropertiesUpdater process all pending vfs events and update file properties before we issue PSI events PushedFilePropertiesUpdater.getInstance(myProject).processPendingEvents(); final VirtualFile vFile = event.getFile(); final PsiDirectory oldParentDir = myFileManager.findDirectory(event.getOldParent()); final PsiDirectory newParentDir = myFileManager.findDirectory(event.getNewParent()); if (oldParentDir == null && newParentDir == null) return; final PsiElement oldElement = vFile.isDirectory() ? myFileManager.getCachedDirectory(vFile) : myFileManager.getCachedPsiFileInner(vFile); myFileManager.removeInvalidFilesAndDirs(true); final PsiElement newElement; final FileViewProvider newViewProvider; if (!vFile.isDirectory()){ newViewProvider = myFileManager.createFileViewProvider(vFile, true); newElement = newViewProvider.getPsi(myFileManager.findViewProvider(vFile).getBaseLanguage()); } else { newElement = myFileManager.findDirectory(vFile); newViewProvider = null; } if (oldElement == null && newElement == null) return; ApplicationManager.getApplication().runWriteAction( new ExternalChangeAction() { @Override public void run() { PsiTreeChangeEventImpl treeEvent = new PsiTreeChangeEventImpl(myManager); if (oldElement == null) { myFileManager.setViewProvider(vFile, newViewProvider); treeEvent.setParent(newParentDir); treeEvent.setChild(newElement); myManager.childAdded(treeEvent); } else { if (newElement == null) { myFileManager.setViewProvider(vFile, null); treeEvent.setParent(oldParentDir); treeEvent.setChild(oldElement); myManager.childRemoved(treeEvent); } else { if (oldElement.getClass().equals(newElement.getClass())) { treeEvent.setOldParent(oldParentDir); treeEvent.setNewParent(newParentDir); treeEvent.setChild(newElement); myManager.childMoved(treeEvent); } else { myFileManager.setViewProvider(vFile, newViewProvider); PsiTreeChangeEventImpl treeRemoveEvent = new PsiTreeChangeEventImpl(myManager); treeRemoveEvent.setParent(oldParentDir); treeRemoveEvent.setChild(oldElement); myManager.childRemoved(treeRemoveEvent); PsiTreeChangeEventImpl treeAddEvent = new PsiTreeChangeEventImpl(myManager); treeAddEvent.setParent(newParentDir); treeAddEvent.setChild(newElement); myManager.childAdded(treeAddEvent); } } } } } ); } @Nullable private PsiFile createFileCopyWithNewName(VirtualFile vFile, String name) { // TODO[ik] remove this. Event handling and generation must be in view providers mechanism since we // need to track changes in _all_ psi views (e.g. namespace changes in XML) final FileTypeManager instance = FileTypeManager.getInstance(); if(instance.isFileIgnored(name)) return null; final FileType fileTypeByFileName = instance.getFileTypeByFileName(name); final Document document = FileDocumentManager.getInstance().getDocument(vFile); return PsiFileFactory.getInstance(myManager.getProject()).createFileFromText(name, fileTypeByFileName, document != null ? document.getCharsSequence() : "", vFile.getModificationStamp(), true, false); } // When file is renamed so that extension changes then language dialect might change and thus psiFile should be invalidated private static boolean languageDialectChanged(final PsiFile newPsiFile, String oldFileName) { return newPsiFile != null && !FileUtilRt.extensionEquals(oldFileName, FileUtilRt.getExtension(newPsiFile.getName())); } private class MyModuleRootListener implements ModuleRootListener { private VirtualFile[] myOldContentRoots = null; private volatile int depthCounter = 0; @Override public void beforeRootsChange(final ModuleRootEvent event) { if (!myFileManager.isInitialized()) return; if (event.isCausedByFileTypesChange()) return; ApplicationManager.getApplication().runWriteAction( new ExternalChangeAction() { @Override public void run() { depthCounter++; if (depthCounter > 1) return; PsiTreeChangeEventImpl treeEvent = new PsiTreeChangeEventImpl(myManager); treeEvent.setPropertyName(PsiTreeChangeEvent.PROP_ROOTS); final VirtualFile[] contentRoots = myProjectRootManager.getContentRoots(); LOG.assertTrue(myOldContentRoots == null); myOldContentRoots = contentRoots; treeEvent.setOldValue(contentRoots); myManager.beforePropertyChange(treeEvent); } } ); } @Override public void rootsChanged(final ModuleRootEvent event) { myFileManager.dispatchPendingEvents(); if (!myFileManager.isInitialized()) return; if (event.isCausedByFileTypesChange()) return; ApplicationManager.getApplication().runWriteAction( new ExternalChangeAction() { @Override public void run() { depthCounter--; assert depthCounter >= 0 : depthCounter; if (depthCounter > 0) return; myFileManager.removeInvalidFilesAndDirs(true); PsiTreeChangeEventImpl treeEvent = new PsiTreeChangeEventImpl(myManager); treeEvent.setPropertyName(PsiTreeChangeEvent.PROP_ROOTS); final VirtualFile[] contentRoots = myProjectRootManager.getContentRoots(); treeEvent.setNewValue(contentRoots); LOG.assertTrue(myOldContentRoots != null); treeEvent.setOldValue(myOldContentRoots); myOldContentRoots = null; myManager.propertyChanged(treeEvent); } } ); } } private class MyFileDocumentManagerAdapter extends FileDocumentManagerAdapter { @Override public void fileWithNoDocumentChanged(@NotNull final VirtualFile file) { final PsiFile psiFile = myFileManager.getCachedPsiFileInner(file); if (psiFile != null) { ApplicationManager.getApplication().runWriteAction( new ExternalChangeAction() { @Override public void run() { if (FileDocumentManagerImpl.recomputeFileTypeIfNecessary(file)) { myFileManager.forceReload(file); } else { myFileManager.reloadFromDisk(psiFile, true); // important to ignore document which might appear already! } } } ); } } } }