private static void after(@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();
      });
    }
  }
private static void after(@NotNull List<? extends VFileEvent> events, Set<File> filesDeleted, Set<File> filesChanged)