private @NotNull CollectedEvents collectEvents(@NotNull List<? extends VFileEvent> events) {
    if (!hasAnyPointers()) {
      // e.g., in some VFS stress tests
      return new CollectedEvents(new MultiMap<>(), List.of(), List.of(), 0, 0);
    }
    long start = System.currentTimeMillis();
    MultiMap<VirtualFilePointerListener, VirtualFilePointerImpl> toFirePointers = MultiMap.create();
    List<NodeToUpdate> toUpdateNodes = new ArrayList<>();

    long startModCount;
    List<EventDescriptor> eventList;
    List<VirtualFilePointer> allPointersToFire;

    //noinspection SynchronizeOnThis
    synchronized (this) {
      startModCount = myPointerSetModCount;
      for (VFileEvent event : events) {
        ProgressManager.checkCanceled();
        VirtualFileSystem vfs = event.getFileSystem();
        if (!(vfs instanceof VirtualFilePointerCapableFileSystem) || !(vfs instanceof NewVirtualFileSystem fs)) continue;
        if (event instanceof VFileDeleteEvent deleteEvent) {
          VirtualFileSystemEntry file = (VirtualFileSystemEntry)deleteEvent.getFile();
          VirtualFileSystemEntry parent = (VirtualFileSystemEntry)FilePartNode.getParentThroughJar(file, file.getFileSystem());
          if (parent != null) {
            addRelevantPointers(file, parent, FilePartNode.getNameId(file), toFirePointers, toUpdateNodes, true, fs, event);
          }
        }
        else if (event instanceof VFileCreateEvent createEvent) {
          boolean fireSubdirectoryPointers;
          if (createEvent.isDirectory()) {
            // when a new empty directory "/a/b" is created, there's no need to fire any deeper pointers like "/a/b/c/d.txt" - they're not created yet
            // OTOH when refresh found a new directory "/a/b" which is non-empty, we must fire deeper pointers because they may exist already
            fireSubdirectoryPointers = !createEvent.isEmptyDirectory();
          }
          else {
            String createdFileName = createEvent.getChildName();
            // if the .jar file created, there may be many files hiding inside
            FileType fileType = FileTypeManager.getInstance().getFileTypeByExtension(FileUtilRt.getExtension(createdFileName));
            fireSubdirectoryPointers = fileType instanceof ArchiveFileType;
          }
          addRelevantPointers(null, (VirtualFileSystemEntry)createEvent.getParent(), createEvent.getChildNameId(), toFirePointers,
                              toUpdateNodes, fireSubdirectoryPointers, fs, event);
        }
        else if (event instanceof VFileCopyEvent copyEvent) {
          addRelevantPointers(null, (VirtualFileSystemEntry)copyEvent.getNewParent(), toNameId(copyEvent.getNewChildName()), toFirePointers,
                              toUpdateNodes, true, fs, event);
        }
        else if (event instanceof VFileMoveEvent moveEvent) {
          VirtualFileSystemEntry eventFile = (VirtualFileSystemEntry)moveEvent.getFile();
          int newNameId = FilePartNode.getNameId(eventFile);
          // files deleted from eventFile and created in moveEvent.getNewParent()
          addRelevantPointers(null, (VirtualFileSystemEntry)moveEvent.getNewParent(), newNameId, toFirePointers, toUpdateNodes, true, fs, event);

          VirtualFileSystemEntry parent = (VirtualFileSystemEntry)FilePartNode.getParentThroughJar(eventFile, eventFile.getFileSystem());
          if (parent != null) {
            addRelevantPointers(eventFile, parent, newNameId, toFirePointers, toUpdateNodes, true, fs, event);
          }
        }
        else if (event instanceof VFilePropertyChangeEvent change) {
          if (VirtualFile.PROP_NAME.equals(change.getPropertyName()) && !Comparing.equal(change.getOldValue(), change.getNewValue())) {
            VirtualFileSystemEntry eventFile = (VirtualFileSystemEntry)change.getFile();
            VirtualFileSystemEntry parent = (VirtualFileSystemEntry)FilePartNode.getParentThroughJar(eventFile, eventFile.getFileSystem());
            // e.g., for LightVirtualFiles
            if (parent != null) {
              int newNameId = toNameId(change.getNewValue().toString());
              addRelevantPointers(eventFile, parent, newNameId, toFirePointers, toUpdateNodes, true, fs, event);

              // old pointers remain valid after rename, no need to fire
              addRelevantPointers(parent, eventFile, FilePartNode.getNameId(eventFile), toFirePointers, toUpdateNodes, true, fs, false, event);
            }
          }
        }
      }

      eventList = new ArrayList<>();
      allPointersToFire = new ArrayList<>();
      groupPointersToFire(toFirePointers, eventList, allPointersToFire);
    }
    if (!allPointersToFire.isEmpty()) {
      VirtualFilePointer[] allPointers = allPointersToFire.toArray(VirtualFilePointer.EMPTY_ARRAY);
      eventList.add(new EventDescriptor(myPublisher, allPointers));
    }
    long prepareElapsedMs = System.currentTimeMillis() - start;

    return new CollectedEvents(toFirePointers, toUpdateNodes, eventList, startModCount, prepareElapsedMs);
  }
private @NotNull CollectedEvents collectEvents(@NotNull List<? extends VFileEvent> events)