package com.jetbrains.lang.dart; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.application.WriteAction; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.*; import com.intellij.openapi.roots.impl.libraries.LibraryEx; import com.intellij.openapi.roots.impl.libraries.LibraryTableBase; import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable; import com.intellij.openapi.roots.libraries.Library; import com.intellij.openapi.roots.libraries.LibraryProperties; import com.intellij.openapi.roots.libraries.LibraryTable; import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.vfs.*; import com.intellij.psi.search.FilenameIndex; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.util.PathUtil; import com.intellij.util.SmartList; import com.jetbrains.lang.dart.sdk.DartPackagesLibraryProperties; import com.jetbrains.lang.dart.sdk.DartPackagesLibraryType; import com.jetbrains.lang.dart.sdk.DartSdkLibUtil; import com.jetbrains.lang.dart.util.DotPackagesFileUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import static com.jetbrains.lang.dart.util.PubspecYamlUtil.PUBSPEC_YAML; public class DartFileListener implements VirtualFileListener { private static final Key<Boolean> DART_PACKAGE_ROOTS_UPDATE_SCHEDULED_OR_IN_PROGRESS = Key.create("DART_PACKAGE_ROOTS_UPDATE_SCHEDULED_OR_IN_PROGRESS"); private final Project myProject; public DartFileListener(Project project) { myProject = project; } @Override public void beforePropertyChange(@NotNull VirtualFilePropertyEvent event) { propertyChanged(event); } @Override public void propertyChanged(@NotNull VirtualFilePropertyEvent event) { if (VirtualFile.PROP_NAME.equals(event.getPropertyName())) { fileChanged(myProject, event.getFile()); } } @Override public void contentsChanged(@NotNull VirtualFileEvent event) { fileChanged(myProject, event.getFile()); } @Override public void fileCreated(@NotNull VirtualFileEvent event) { fileChanged(myProject, event.getFile()); } @Override public void fileDeleted(@NotNull VirtualFileEvent event) { fileChanged(myProject, event.getFile()); } @Override public void fileMoved(@NotNull VirtualFileMoveEvent event) { fileChanged(myProject, event.getFile()); } @Override public void fileCopied(@NotNull VirtualFileCopyEvent event) { fileChanged(myProject, event.getFile()); } private static void fileChanged(@NotNull final Project project, @NotNull final VirtualFile file) { if (!DotPackagesFileUtil.DOT_PACKAGES.equals(file.getName())) return; if (LocalFileSystem.getInstance() != file.getFileSystem() && !ApplicationManager.getApplication().isUnitTestMode()) return; final VirtualFile parent = file.getParent(); final VirtualFile pubspec = parent == null ? null : parent.findChild(PUBSPEC_YAML); if (pubspec != null) { scheduleDartPackageRootsUpdate(project); final Module module = ModuleUtilCore.findModuleForFile(pubspec, project); if (module != null && !module.isDisposed() && !project.isDisposed()) { DartProjectComponent.excludeBuildAndPackagesFolders(module, pubspec); } } } /** * Make sure to set it to <code>false</code> in the corresponding <code>finally</code> block */ public static void setDartPackageRootUpdateScheduledOrInProgress(@NotNull final Project project, final boolean scheduledOrInProgress) { if (scheduledOrInProgress) { project.putUserData(DART_PACKAGE_ROOTS_UPDATE_SCHEDULED_OR_IN_PROGRESS, true); } else { project.putUserData(DART_PACKAGE_ROOTS_UPDATE_SCHEDULED_OR_IN_PROGRESS, null); } } public static void scheduleDartPackageRootsUpdate(@NotNull final Project project) { if (Registry.is("dart.projects.without.pubspec", false)) return; if (project.getUserData(DART_PACKAGE_ROOTS_UPDATE_SCHEDULED_OR_IN_PROGRESS) == Boolean.TRUE) { return; } setDartPackageRootUpdateScheduledOrInProgress(project, Boolean.TRUE); final Runnable runnable = () -> { try { final Library library = actualizePackagesLibrary(project); if (library == null) { removeDartPackagesLibraryAndDependencies(project); } else { final Condition<Module> moduleFilter = DartSdkLibUtil::isDartSdkEnabled; updateDependenciesOnDartPackagesLibrary(project, moduleFilter, library); } } finally { setDartPackageRootUpdateScheduledOrInProgress(project, false); } }; if (ApplicationManager.getApplication().isUnitTestMode()) { runnable.run(); } else { DumbService.getInstance(project).smartInvokeLater(runnable, ModalityState.NON_MODAL); } } @Nullable private static Library actualizePackagesLibrary(@NotNull final Project project) { final DartLibInfo libInfo = collectPackagesLibraryRoots(project); if (libInfo.getLibRootUrls().isEmpty()) { return null; } else { return updatePackagesLibraryRoots(project, libInfo); } } @NotNull private static DartLibInfo collectPackagesLibraryRoots(@NotNull final Project project) { final DartLibInfo libInfo = new DartLibInfo(false); final Collection<VirtualFile> pubspecYamlFiles = FilenameIndex.getVirtualFilesByName(project, PUBSPEC_YAML, GlobalSearchScope.projectScope(project)); final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(project).getFileIndex(); for (VirtualFile pubspecFile : pubspecYamlFiles) { final VirtualFile dotPackagesFile = pubspecFile.getParent().findChild(DotPackagesFileUtil.DOT_PACKAGES); final Module module = dotPackagesFile == null ? null : fileIndex.getModuleForFile(dotPackagesFile); if (dotPackagesFile != null && !dotPackagesFile.isDirectory() && module != null && DartSdkLibUtil.isDartSdkEnabled(module)) { final Map<String, String> packagesMap = DotPackagesFileUtil.getPackagesMap(dotPackagesFile); if (packagesMap != null) { for (Map.Entry<String, String> entry : packagesMap.entrySet()) { final String packageName = entry.getKey(); final String packagePath = entry.getValue(); if (isPathOutsideProjectContent(fileIndex, packagePath)) { libInfo.addPackage(packageName, packagePath); } } } } } return libInfo; } @NotNull public static Library updatePackagesLibraryRoots(@NotNull final Project project, @NotNull final DartLibInfo libInfo) { final LibraryTable projectLibraryTable = ProjectLibraryTable.getInstance(project); final Library existingLibrary = projectLibraryTable.getLibraryByName(DartPackagesLibraryType.DART_PACKAGES_LIBRARY_NAME); final Library library = existingLibrary != null ? existingLibrary : WriteAction.compute(() -> { final LibraryTableBase.ModifiableModel libTableModel = ProjectLibraryTable.getInstance(project).getModifiableModel(); final Library lib = libTableModel .createLibrary(DartPackagesLibraryType.DART_PACKAGES_LIBRARY_NAME, DartPackagesLibraryType.LIBRARY_KIND); libTableModel.commit(); return lib; }); final String[] existingUrls = library.getUrls(OrderRootType.CLASSES); final Collection<String> libRootUrls = libInfo.getLibRootUrls(); if ((!libInfo.isProjectWithoutPubspec() && isBrokenPackageMap(((LibraryEx)library).getProperties())) || existingUrls.length != libRootUrls.size() || !libRootUrls.containsAll(Arrays.asList(existingUrls))) { ApplicationManager.getApplication().runWriteAction(() -> { final LibraryEx.ModifiableModelEx model = (LibraryEx.ModifiableModelEx)library.getModifiableModel(); for (String url : existingUrls) { model.removeRoot(url, OrderRootType.CLASSES); } for (String url : libRootUrls) { model.addRoot(url, OrderRootType.CLASSES); } final DartPackagesLibraryProperties libraryProperties = new DartPackagesLibraryProperties(); libraryProperties.setPackageNameToDirsMap(libInfo.getPackagesMap()); model.setProperties(libraryProperties); model.commit(); }); } return library; } private static boolean isBrokenPackageMap(@Nullable final LibraryProperties properties) { if (!(properties instanceof DartPackagesLibraryProperties)) return true; for (Map.Entry<String, List<String>> entry : ((DartPackagesLibraryProperties)properties).getPackageNameToDirsMap().entrySet()) { if (entry == null || entry.getKey() == null || entry.getValue() == null) { return true; } } return false; } private static void removeDartPackagesLibraryAndDependencies(@NotNull final Project project) { for (Module module : ModuleManager.getInstance(project).getModules()) { removeDependencyOnDartPackagesLibrary(module); } final Library library = ProjectLibraryTable.getInstance(project).getLibraryByName(DartPackagesLibraryType.DART_PACKAGES_LIBRARY_NAME); if (library != null) { ApplicationManager.getApplication().runWriteAction(() -> ProjectLibraryTable.getInstance(project).removeLibrary(library)); } } public static void updateDependenciesOnDartPackagesLibrary(@NotNull final Project project, @NotNull final Condition<Module> moduleFilter, @NotNull final Library library) { for (Module module : ModuleManager.getInstance(project).getModules()) { if (moduleFilter.value(module)) { addDependencyOnDartPackagesLibrary(module, library); } else { removeDependencyOnDartPackagesLibrary(module); } } } private static void removeDependencyOnDartPackagesLibrary(@NotNull final Module module) { final ModifiableRootModel modifiableModel = ModuleRootManager.getInstance(module).getModifiableModel(); try { for (final OrderEntry orderEntry : modifiableModel.getOrderEntries()) { if (orderEntry instanceof LibraryOrderEntry && LibraryTablesRegistrar.PROJECT_LEVEL.equals(((LibraryOrderEntry)orderEntry).getLibraryLevel()) && DartPackagesLibraryType.DART_PACKAGES_LIBRARY_NAME.equals(((LibraryOrderEntry)orderEntry).getLibraryName())) { modifiableModel.removeOrderEntry(orderEntry); } } if (modifiableModel.isChanged()) { ApplicationManager.getApplication().runWriteAction(modifiableModel::commit); } } finally { if (!modifiableModel.isDisposed()) { modifiableModel.dispose(); } } } private static void addDependencyOnDartPackagesLibrary(@NotNull final Module module, @NotNull final Library library) { final ModifiableRootModel modifiableModel = ModuleRootManager.getInstance(module).getModifiableModel(); try { for (final OrderEntry orderEntry : modifiableModel.getOrderEntries()) { if (orderEntry instanceof LibraryOrderEntry && LibraryTablesRegistrar.PROJECT_LEVEL.equals(((LibraryOrderEntry)orderEntry).getLibraryLevel()) && DartPackagesLibraryType.DART_PACKAGES_LIBRARY_NAME.equals(((LibraryOrderEntry)orderEntry).getLibraryName())) { return; // dependency already exists } } modifiableModel.addLibraryEntry(library); ApplicationManager.getApplication().runWriteAction(modifiableModel::commit); } finally { if (!modifiableModel.isDisposed()) { modifiableModel.dispose(); } } } private static boolean isPathOutsideProjectContent(@NotNull final ProjectFileIndex fileIndex, @NotNull String path) { if (ApplicationManager.getApplication().isUnitTestMode() && path.contains("/pub/global/cache/")) { return true; } while (!path.isEmpty()) { final VirtualFile file = LocalFileSystem.getInstance().findFileByPath(path); if (file == null) { path = PathUtil.getParentPath(path); } else { return !fileIndex.isInContent(file); } } return false; } public static class DartLibInfo { private final boolean myProjectWithoutPubspec; private final Set<String> myLibRootUrls = new TreeSet<>(); private final Map<String, List<String>> myPackagesMap = new TreeMap<>(); public DartLibInfo(final boolean projectWithoutPubspec) { myProjectWithoutPubspec = projectWithoutPubspec; } private void addPackage(@NotNull final String packageName, @NotNull final String packagePath) { myLibRootUrls.add(VfsUtilCore.pathToUrl(packagePath)); List<String> paths = myPackagesMap.get((packageName)); if (paths == null) { paths = new SmartList<>(); myPackagesMap.put(packageName, paths); } if (!paths.contains(packagePath)) { paths.add(packagePath); } } public void addRoots(final Collection<String> dirPaths) { for (String path : dirPaths) { myLibRootUrls.add(VfsUtilCore.pathToUrl(path)); } } public boolean isProjectWithoutPubspec() { return myProjectWithoutPubspec; } public Set<String> getLibRootUrls() { return myLibRootUrls; } public Map<String, List<String>> getPackagesMap() { return myPackagesMap; } } }