/* * 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.openapi.vcs.changes; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.ApplicationComponent; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectLocator; import com.intellij.openapi.util.Couple; import com.intellij.openapi.vcs.ConstantZipperUpdater; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.FilePathImpl; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.intellij.openapi.vfs.newvfs.BulkFileListener; import com.intellij.openapi.vfs.newvfs.events.*; import com.intellij.util.Alarm; import com.intellij.util.containers.HashMap; import com.intellij.util.containers.HashSet; import com.intellij.util.messages.MessageBusConnection; import com.intellij.vcsUtil.VcsUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; /** * Listens to file system events and notifies VcsDirtyScopeManagers responsible for changed files to mark these files dirty. * * @author Irina Chernushina * @author Kirill Likhodedov */ public class VcsDirtyScopeVfsListener implements ApplicationComponent, BulkFileListener { private final ProjectLocator myProjectLocator; private final MessageBusConnection myMessageBusConnection; // for tests only private boolean myForbid; private final ConstantZipperUpdater myZipperUpdater; private final List myQueue; private final Object myLock; private final Runnable myDirtReporter; public VcsDirtyScopeVfsListener() { myProjectLocator = ProjectLocator.getInstance(); myMessageBusConnection = ApplicationManager.getApplication().getMessageBus().connect(); myLock = new Object(); myQueue = new ArrayList(); myDirtReporter = new Runnable() { @Override public void run() { ArrayList list; synchronized (myLock) { list = new ArrayList(myQueue); myQueue.clear(); } Map>> map = new HashMap>>(); for (FileAndDirsCollector collector : list) { Map>> pairMap = collector.map; for (Map.Entry>> entry : pairMap .entrySet()) { final VcsDirtyScopeManager key = entry.getKey(); Couple> existing = map.get(key); Couple> value = entry.getValue(); if (existing != null) { existing.getFirst().addAll(value.getFirst()); existing.getSecond().addAll(value.getSecond()); } else { map.put(key, value); } } } new FileAndDirsCollector().markDirty(map); } }; myZipperUpdater = new ConstantZipperUpdater(300, Alarm.ThreadToUse.POOLED_THREAD, ApplicationManager.getApplication(), myDirtReporter); } public void setForbid(boolean forbid) { assert ApplicationManager.getApplication().isUnitTestMode(); myForbid = forbid; } public void flushDirt() { myDirtReporter.run(); } @Override @NotNull public String getComponentName() { return VcsDirtyScopeVfsListener.class.getName(); } @Override public void initComponent() { myMessageBusConnection.subscribe(VirtualFileManager.VFS_CHANGES, this); } @Override public void disposeComponent() { myMessageBusConnection.disconnect(); } @Override public void before(@NotNull List events) { if (myForbid) return; final FileAndDirsCollector dirtyFilesAndDirs = new FileAndDirsCollector(); // collect files and directories - sources of events for (VFileEvent event : events) { final VirtualFile file = getFileForEvent(event); if (file == null) { continue; } if (event instanceof VFileDeleteEvent) { if (!file.isInLocalFileSystem()) { continue; } dirtyFilesAndDirs.add(file, true); } else if (event instanceof VFileMoveEvent || event instanceof VFilePropertyChangeEvent) { dirtyFilesAndDirs.add(file, true); } } // and notify VCSDirtyScopeManager markDirtyOnPooled(dirtyFilesAndDirs); } @Override public void after(@NotNull List events) { if (myForbid) return; final FileAndDirsCollector dirtyFilesAndDirs = new FileAndDirsCollector(); // collect files and directories - sources of events for (VFileEvent event : events) { if (event instanceof VFileDeleteEvent) continue; final VirtualFile file = getFileForEvent(event); if (file == null) { continue; } if (event instanceof VFileContentChangeEvent || event instanceof VFileCopyEvent || event instanceof VFileCreateEvent || event instanceof VFileMoveEvent) { dirtyFilesAndDirs.add(file, false); } else if (event instanceof VFilePropertyChangeEvent) { final VFilePropertyChangeEvent pce = (VFilePropertyChangeEvent) event; if (pce.getPropertyName().equals(VirtualFile.PROP_NAME)) { // if a file was renamed, then the file is dirty and its parent directory is dirty too; // if a directory was renamed, all its children are recursively dirty, the parent dir is also dirty but not recursively. dirtyFilesAndDirs.add(file, false); // the file is dirty recursively dirtyFilesAndDirs.addToFiles(file.getParent(), false); // directory is dirty alone. if parent is null - is checked in the method } else { dirtyFilesAndDirs.addToFiles(file, false); } } } // and notify VCSDirtyScopeManager markDirtyOnPooled(dirtyFilesAndDirs); } private void markDirtyOnPooled(final FileAndDirsCollector dirtyFilesAndDirs) { synchronized (myLock) { myQueue.add(dirtyFilesAndDirs); } myZipperUpdater.request(); } @Nullable private static VirtualFile getFileForEvent(VFileEvent event) { return VcsUtil.getVirtualFile(event.getPath()); } /** * Stores VcsDirtyScopeManagers and files and directories which should be marked dirty by them. * Files will be marked dirty, directories will be marked recursively dirty, so if you need to mark dirty a directory, but * not recursively, you should add it to files. */ private class FileAndDirsCollector { // dirty scope manager -> Pair(set of dirty files, set of dirty directories) Map>> map = new HashMap>>(); /** * For the given VirtualFile constructs a FilePathImpl object without referring to the initial VirtualFile object * and adds this FilePathImpl to the set of files for proper VcsDirtyScopeManager - to mark these files dirty * when the set will be populated. * @param file file which path is to be added. * @param addToFiles If true, then add to dirty files even if it is a directory. Otherwise add to the proper set. */ private void add(VirtualFile file, boolean addToFiles, final boolean forDelete) { if (file == null) { return; } final boolean isDirectory = file.isDirectory(); // need to create FilePath explicitly without referring to VirtualFile because the path of VirtualFile may change final FilePathImpl path = forDelete ? new FilePathImpl(new File(file.getPath()), isDirectory) : new FilePathImpl(file); final Collection managers = getManagers(file); for (VcsDirtyScopeManager manager : managers) { Couple> filesAndDirs = map.get(manager); if (filesAndDirs == null) { filesAndDirs = Couple.of(new HashSet(), new HashSet()); map.put(manager, filesAndDirs); } if (addToFiles || !isDirectory) { filesAndDirs.first.add(path); } else { filesAndDirs.second.add(path); } } } /** * Adds files to the collection of files and directories - to the collection of directories (which are handled recursively). */ private void add(VirtualFile file, final boolean forDelete) { add(file, false, forDelete); } /** * Adds to the collection of files. A file (even if it is a directory) is marked dirty alone (not recursively). * Use this method, when you want directory not to be marked dirty recursively. */ private void addToFiles(VirtualFile file, final boolean forDelete) { add(file, true, forDelete); } private void markDirty(final Map>> outerMap) { for (Map.Entry>> entry : outerMap.entrySet()) { VcsDirtyScopeManager manager = entry.getKey(); HashSet files = entry.getValue().first; HashSet dirs = entry.getValue().second; manager.filePathsDirty(files, dirs); } } } /** * Returns all VcsDirtyScopeManagers which serve the given file. * There may be none of them or there may be several (if a file is contained in several open projects, for instance), * though usually there is one. */ @NotNull private Collection getManagers(final VirtualFile file) { final Collection result = new HashSet(); if (file == null) { return result; } ApplicationManager.getApplication().runReadAction(new Runnable() { @Override public void run() { final Collection projects = myProjectLocator.getProjectsForFile(file); for (Project project : projects) { final VcsDirtyScopeManager manager = VcsDirtyScopeManager.getInstance(project); if (manager != null && project.isInitialized()) { result.add(manager); } } } }); return result; } }