package com.jetbrains.lang.dart;
import com.intellij.ProjectTopics;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.AbstractProjectComponent;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.impl.ModifiableModelCommitter;
import com.intellij.openapi.roots.impl.libraries.ApplicationLibraryTable;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.ModificationTracker;
import com.intellij.openapi.util.SimpleModificationTracker;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.VirtualFileVisitor;
import com.intellij.psi.search.FileTypeIndex;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.lang.dart.ide.runner.client.DartiumUtil;
import com.jetbrains.lang.dart.sdk.DartSdk;
import com.jetbrains.lang.dart.sdk.DartSdkLibUtil;
import com.jetbrains.lang.dart.sdk.DartSdkUtil;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import static com.jetbrains.lang.dart.util.PubspecYamlUtil.PUBSPEC_YAML;
public class DartProjectComponent extends AbstractProjectComponent {
private SimpleModificationTracker myProjectRootsModificationTracker = new SimpleModificationTracker();
protected DartProjectComponent(@NotNull final Project project) {
super(project);
VirtualFileManager.getInstance().addVirtualFileListener(new DartFileListener(project), project);
project.getMessageBus().connect().subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootListener() {
@Override
public void rootsChanged(ModuleRootEvent event) {
myProjectRootsModificationTracker.incModificationCount();
if (!Registry.is("dart.projects.without.pubspec", false)) {
DartFileListener.scheduleDartPackageRootsUpdate(myProject);
}
}
});
}
@NotNull
public static ModificationTracker getProjectRootsModificationTracker(@NotNull final Project project) {
if (project.isDefault()) {
return ModificationTracker.NEVER_CHANGED;
}
// standard ProjectRootManager (that is a ModificationTracker itself) doesn't work as its modificationCount is not incremented when library root is deleted
final DartProjectComponent component = project.getComponent(DartProjectComponent.class);
assert component != null;
return component.myProjectRootsModificationTracker;
}
public void projectOpened() {
StartupManager.getInstance(myProject).runWhenProjectIsInitialized(() -> {
removeGlobalDartSdkLib();
convertOrderEntriesTargetingGlobalDartSdkLib();
DartiumUtil.resetDartiumFlags();
final Collection<VirtualFile> pubspecYamlFiles =
FilenameIndex.getVirtualFilesByName(myProject, PUBSPEC_YAML, GlobalSearchScope.projectScope(myProject));
for (VirtualFile pubspecYamlFile : pubspecYamlFiles) {
final Module module = ModuleUtilCore.findModuleForFile(pubspecYamlFile, myProject);
if (module != null && FileTypeIndex.containsFileOfType(DartFileType.INSTANCE, module.getModuleContentScope())) {
excludeBuildAndPackagesFolders(module, pubspecYamlFile);
}
}
});
}
private void removeGlobalDartSdkLib() {
for (final Library library : ApplicationLibraryTable.getApplicationTable().getLibraries()) {
if (DartSdk.DART_SDK_LIB_NAME.equals(library.getName())) {
final DartSdk oldGlobalSdk = DartSdk.getSdkByLibrary(library);
if (oldGlobalSdk != null) {
DartSdkUtil.updateKnownSdkPaths(myProject, oldGlobalSdk.getHomePath());
}
ApplicationManager.getApplication().runWriteAction(() -> ApplicationLibraryTable.getApplicationTable().removeLibrary(library));
return;
}
}
}
private void convertOrderEntriesTargetingGlobalDartSdkLib() {
final DartSdk correctSdk = DartSdk.getDartSdk(myProject);
if (correctSdk != null) return; // already converted
// for performance reasons avoid taking write action and modifiable models if not needed
if (!hasIncorrectModuleDependencies()) return;
final String sdkPath = DartSdkUtil.getFirstKnownDartSdkPath();
if (sdkPath == null) return;
ApplicationManager.getApplication().runWriteAction(() -> {
DartSdkLibUtil.ensureDartSdkConfigured(myProject, sdkPath);
final Collection<ModifiableRootModel> modelsToCommit = new SmartList<>();
for (final Module module : ModuleManager.getInstance(myProject).getModules()) {
boolean hasCorrectDependency = false;
boolean needsCorrectDependency = false;
final List<OrderEntry> orderEntriesToRemove = new SmartList<>();
final ModifiableRootModel model = ModuleRootManager.getInstance(module).getModifiableModel();
for (final OrderEntry orderEntry : model.getOrderEntries()) {
if (isOldGlobalDartSdkLibEntry(orderEntry)) {
needsCorrectDependency = true;
orderEntriesToRemove.add(orderEntry);
}
else if (DartSdkLibUtil.isDartSdkOrderEntry(orderEntry)) {
hasCorrectDependency = true;
}
}
if (needsCorrectDependency && !hasCorrectDependency || !orderEntriesToRemove.isEmpty()) {
if (needsCorrectDependency && !hasCorrectDependency) {
model.addInvalidLibrary(DartSdk.DART_SDK_LIB_NAME, LibraryTablesRegistrar.PROJECT_LEVEL);
}
for (OrderEntry entry : orderEntriesToRemove) {
model.removeOrderEntry(entry);
}
modelsToCommit.add(model);
}
else {
model.dispose();
}
}
commitModifiableModels(myProject, modelsToCommit);
});
}
public static void commitModifiableModels(@NotNull final Project project, @NotNull final Collection<ModifiableRootModel> modelsToCommit) {
if (!modelsToCommit.isEmpty()) {
try {
ModifiableModelCommitter.multiCommit(modelsToCommit, ModuleManager.getInstance(project).getModifiableModel());
}
finally {
for (ModifiableRootModel model : modelsToCommit) {
if (!model.isDisposed()) {
model.dispose();
}
}
}
}
}
private boolean hasIncorrectModuleDependencies() {
for (final Module module : ModuleManager.getInstance(myProject).getModules()) {
for (final OrderEntry orderEntry : ModuleRootManager.getInstance(module).getOrderEntries()) {
if (isOldGlobalDartSdkLibEntry(orderEntry)) return true;
}
}
return false;
}
private static boolean isOldGlobalDartSdkLibEntry(OrderEntry orderEntry) {
if (orderEntry instanceof LibraryOrderEntry &&
LibraryTablesRegistrar.APPLICATION_LEVEL.equals(((LibraryOrderEntry)orderEntry).getLibraryLevel()) &&
DartSdk.DART_SDK_LIB_NAME.equals(((LibraryOrderEntry)orderEntry).getLibraryName())) {
return true;
}
return false;
}
public static void excludeBuildAndPackagesFolders(final @NotNull Module module, final @NotNull VirtualFile pubspecYamlFile) {
final VirtualFile root = pubspecYamlFile.getParent();
final VirtualFile contentRoot =
root == null ? null : ProjectRootManager.getInstance(module.getProject()).getFileIndex().getContentRootForFile(root);
if (contentRoot == null) return;
// http://pub.dartlang.org/doc/glossary.html#entrypoint-directory
// Entrypoint directory: A directory inside your package that is allowed to contain Dart entrypoints.
// Pub will ensure all of these directories get a "packages" directory, which is needed for "package:" imports to work.
// Pub has a whitelist of these directories: benchmark, bin, example, test, tool, and web.
// Any subdirectories of those (except bin) may also contain entrypoints.
//
// the same can be seen in the pub tool source code: [repo root]/sdk/lib/_internal/pub/lib/src/entrypoint.dart
final Collection<String> oldExcludedUrls =
ContainerUtil.filter(ModuleRootManager.getInstance(module).getExcludeRootUrls(), new Condition<String>() {
final String rootUrl = root.getUrl();
public boolean value(final String url) {
if (url.equals(rootUrl + "/.pub")) return true;
if (url.equals(rootUrl + "/build")) return true;
if (url.equals(rootUrl + "/packages")) return true;
// excluded subfolder of the root 'packages' folder (older versions of the Dart plugin)
if (url.startsWith(root + "/packages/")) return true;
if (url.endsWith("/packages") && (url.startsWith(rootUrl + "/bin/") ||
url.startsWith(rootUrl + "/benchmark/") ||
url.startsWith(rootUrl + "/example/") ||
url.startsWith(rootUrl + "/test/") ||
url.startsWith(rootUrl + "/tool/") |
url.startsWith(rootUrl + "/web/"))) {
return true;
}
return false;
}
});
final Set<String> newExcludedUrls = collectFolderUrlsToExclude(module, pubspecYamlFile);
if (oldExcludedUrls.size() != newExcludedUrls.size() || !newExcludedUrls.containsAll(oldExcludedUrls)) {
ModuleRootModificationUtil.updateExcludedFolders(module, contentRoot, oldExcludedUrls, newExcludedUrls);
}
}
private static Set<String> collectFolderUrlsToExclude(@NotNull final Module module,
@NotNull final VirtualFile pubspecYamlFile) {
final THashSet<String> newExcludedPackagesUrls = new THashSet<>();
final VirtualFile root = pubspecYamlFile.getParent();
newExcludedPackagesUrls.add(root.getUrl() + "/.pub");
newExcludedPackagesUrls.add(root.getUrl() + "/build");
newExcludedPackagesUrls.addAll(getExcludedPackageSymlinkUrls(module.getProject(), root));
return newExcludedPackagesUrls;
}
public static THashSet<String> getExcludedPackageSymlinkUrls(@NotNull final Project project, @NotNull final VirtualFile dartProjectRoot) {
final THashSet<String> result = new THashSet<>();
final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(project).getFileIndex();
// java.io.File is used intentionally, VFS may not yet know these files at this point
if (new File(dartProjectRoot.getPath() + "/packages").isDirectory()) {
result.add(dartProjectRoot.getUrl() + "/packages");
}
final VirtualFile binFolder = dartProjectRoot.findChild("bin");
if (binFolder != null && binFolder.isDirectory() && fileIndex.isInContent(binFolder)) {
if (new File(binFolder.getPath() + "/packages").isDirectory()) {
result.add(binFolder.getUrl() + "/packages");
}
}
appendPackagesFolders(result, dartProjectRoot.findChild("benchmark"), fileIndex);
appendPackagesFolders(result, dartProjectRoot.findChild("example"), fileIndex);
appendPackagesFolders(result, dartProjectRoot.findChild("test"), fileIndex);
appendPackagesFolders(result, dartProjectRoot.findChild("tool"), fileIndex);
appendPackagesFolders(result, dartProjectRoot.findChild("web"), fileIndex);
return result;
}
private static void appendPackagesFolders(final @NotNull Collection<String> excludedPackagesUrls,
final @Nullable VirtualFile folder,
final @NotNull ProjectFileIndex fileIndex) {
if (folder == null) return;
VfsUtilCore.visitChildrenRecursively(folder, new VirtualFileVisitor() {
@NotNull
public Result visitFileEx(@NotNull final VirtualFile file) {
if (!fileIndex.isInContent(file)) {
return SKIP_CHILDREN;
}
if (file.isDirectory()) {
if ("packages".equals(file.getName())) {
return SKIP_CHILDREN;
}
else {
// java.io.File is used intentionally, VFS may not yet know these files at this point
if (new File(file.getPath() + "/packages").isDirectory()) {
excludedPackagesUrls.add(file.getUrl() + "/packages");
}
}
}
return CONTINUE;
}
});
}
}