void (@NotNull List<? extends VFileEvent> events, Set<File> filesDeleted, Set<File> filesChanged) { var changedFilesCollector = new Consumer<VirtualFile>() { private final Set<VirtualFile> dirsToTraverse = new SmartHashSet<>(); @Override public void accept(VirtualFile file) { if (file != null) { if (file.isDirectory()) { // need to traverse directories on IDE side, because JPS build // has less knowledge to efficiently traverse the directory up-down according to the project layout dirsToTraverse.add(file); } else { collectPaths(file, filesChanged, false); } } } public boolean traverseDirs() throws ProcessCanceledException{ Set<VirtualFile> processed = new SmartHashSet<>(); try { for (VirtualFile root : dirsToTraverse) { if (!root.isValid()) { if (LOG.isDebugEnabled()) { LOG.debug("File invalidated while processing VFS events, clearing build state: " + root.getPath()); } return false; } collectPaths(root, filesChanged, true); processed.add(root); } } finally { dirsToTraverse.removeAll(processed); } return true; } public boolean hasPaths() { return !filesChanged.isEmpty() || !dirsToTraverse.isEmpty(); } }; for (VFileEvent event : events) { if (event instanceof VFileMoveEvent || event instanceof VFileCreateEvent) { changedFilesCollector.accept(event.getFile()); } else if (event instanceof VFileCopyEvent copyEvent) { changedFilesCollector.accept(copyEvent.findCreatedFile()); } else { handleFileRename(event, e -> changedFilesCollector.accept(e.getFile())); } } // If a file name differs ony in case, on case-insensitive file systems such name still denotes the same file. // In this situation filesDeleted and filesChanged sets will contain paths which are different only in case. // Thus, the order in which BuildManager is notified, is important: // first deleted paths notification and only then changed paths notification BuildManager buildManager = BuildManager.getInstance(); buildManager.notifyFilesDeleted(filesDeleted); if (changedFilesCollector.hasPaths()) { buildManager.notifyFilesChanged(() -> { // traversing dirs may take time and block UI thread, so traversing them in a non-blocking read-action in background if (changedFilesCollector.dirsToTraverse.isEmpty()) { return filesChanged; } if (ReadAction.nonBlocking(changedFilesCollector::traverseDirs).executeSynchronously()) { return filesChanged; } // force FS rescan on the next build, because information from event may be incomplete buildManager.clearState(); return Collections.emptyList(); }); } }