Skip to content

Commit

Permalink
feat: Ignore CMake cache directories (bazelbuild#124)
Browse files Browse the repository at this point in the history
Add an Action that modifies the local project view to ignore the CMake
cache directories.

Can be triggered manually, and it will trigger automatically on every
sync.
  • Loading branch information
Borja Lorente Escobar authored and Borja Lorente committed Oct 11, 2023
1 parent 6600e3c commit 6e65698
Show file tree
Hide file tree
Showing 12 changed files with 313 additions and 15 deletions.
17 changes: 15 additions & 2 deletions apple/src/META-INF/apple.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,27 @@
class="com.apple.idea.extensions.actions.FileRadar"
text="File a Radar"
icon="apple.src.icons.AppleIcons.Radar"
>
</action>
/>
<action
id="Apple.Bazel.ExcludeDirectoryInProjectView"
class="com.apple.idea.extensions.actions.ExcludeDirectoryInProjectView"
text="Exclude Directory in Project View and Resync"
icon="AllIcons.Modules.ExcludeRoot"
/>
</actions>

<extensions defaultExtensionNs="com.intellij">
<notificationGroup
displayType="BALLOON"
id="Apple.Bazel.NotificationGroup"
/>
<editorNotificationProvider
implementation="com.apple.idea.extensions.actions.ExcludeDirectoryInProjectView$FileOpenListener"
/>
</extensions>
<extensions defaultExtensionNs="com.google.idea.blaze">
<SyncListener
implementation="com.apple.idea.extensions.actions.ExcludeDirectoryInProjectView$ExcludeCmakeCachedirsSyncListener"
/>
</extensions>
</idea-plugin>
229 changes: 229 additions & 0 deletions apple/src/com/actions/ExcludeDirectoryInProjectView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package com.apple.idea.extensions.actions;

import com.google.common.collect.ImmutableSet;
import com.google.idea.blaze.base.actions.BlazeProjectAction;
import com.google.idea.blaze.base.async.FutureUtil;
import com.google.idea.blaze.base.model.primitives.WorkspacePath;
import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
import com.google.idea.blaze.base.projectview.ProjectViewEdit;
import com.google.idea.blaze.base.projectview.ProjectViewManager;
import com.google.idea.blaze.base.projectview.ProjectViewSet;
import com.google.idea.blaze.base.projectview.section.ListSection;
import com.google.idea.blaze.base.projectview.section.sections.DirectoryEntry;
import com.google.idea.blaze.base.projectview.section.sections.DirectorySection;
import com.google.idea.blaze.base.scope.BlazeContext;
import com.google.idea.blaze.base.settings.ui.OpenProjectViewAction;
import com.google.idea.blaze.base.sync.BlazeSyncManager;
import com.google.idea.blaze.base.sync.SyncListener;
import com.google.idea.blaze.base.sync.SyncMode;
import com.google.idea.blaze.base.sync.SyncResult;
import com.google.idea.blaze.base.sync.projectstructure.DirectoryStructure;
import com.google.idea.blaze.base.sync.projectview.ImportRoots;
import com.google.idea.sdkcompat.general.EditorNotificationCompat;
import com.intellij.notification.NotificationAction;
import com.intellij.notification.NotificationGroupManager;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.FileIndexFacade;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.EditorNotificationPanel;
import com.intellij.ui.EditorNotificationProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.io.File;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

/**
* Action to exclude CMake directories.
* It can be triggered in several ways:
* - Manually, via dropdown menus or right-cliking on the file name.
* - Triggered automatically when you open a file that should be excluded.
* - Triggered automatically (in the background) at the end of every sync.
*/
public class ExcludeDirectoryInProjectView extends BlazeProjectAction {
private static final String CMAKE_CACHE_DIR_MARKER = "CMakeCache.txt";
private static final String IGNORED_DIRECTORIES_NOTIFICATION_MSG = "Some directories were identified as CMake files and excluded. Go to the project view for the full list.";

@Override
protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
VirtualFile vf = e.getData(PlatformDataKeys.VIRTUAL_FILE);
WorkspaceRoot wsRoot = WorkspaceRoot.fromProject(project);
if (vf == null || !wsRoot.isInWorkspace(vf)) {
return;
}
WorkspacePath wsPath = wsRoot.workspacePathFor(vf);
markIgnoredAndResync(project, wsPath);
}

/**
* Will be triggered when we open a file.
* If we detect the file to be inside a CMake cache directory, we will ask the user to ignore it.
*/
public static class FileOpenListener implements EditorNotificationProvider {

@Override
public @Nullable Function<FileEditor, JComponent> collectNotificationData(@NotNull Project project, @NotNull VirtualFile virtualFile) {
WorkspaceRoot wsRoot = WorkspaceRoot.fromProject(project);
Optional<VirtualFile> maybeCmakeCacheDir = findParentCmakeCacheDir(wsRoot, virtualFile);
if (maybeCmakeCacheDir.isEmpty()) {
return null;
}
VirtualFile cmakeCacheDir = maybeCmakeCacheDir.get();
FileIndexFacade fileIndex = FileIndexFacade.getInstance(project);
if (fileIndex.isExcludedFile(cmakeCacheDir)) {
return null;
}

return fileEditor -> {
EditorNotificationPanel panel = EditorNotificationCompat.Warning(fileEditor);
panel.setText("It looks like this file belongs to a CMake cache directory. Please mark it as ignored.");
panel.createActionLabel("Exclude and Resync", () -> {
markIgnoredAndResync(project, wsRoot.workspacePathFor(cmakeCacheDir));
});
return panel;
};
}
}

/**
* Triggered after every sync, it will walk the import roots (what you have specified in `directories`)
* and gather everything that looks like a CMake cache directory.
* Then, it will mark them as ignored and trigger a sync.
* The traversal is done as an interruptible process in the background, so there should be no performance implication.
*/
public static class ExcludeCmakeCachedirsSyncListener implements SyncListener {

private static final Logger logger =
Logger.getInstance(ExcludeCmakeCachedirsSyncListener.class);

private HashSet<WorkspacePath> walkDirectories(WorkspacePath current, DirectoryStructure ds, Collection<WorkspacePath> excludedDirectories, WorkspaceRoot wsRoot) {
if (excludedDirectories.contains(current)) {
return new HashSet<>();
}
if (isCmakeCacheDir(wsRoot.fileForPath(current))) {
HashSet<WorkspacePath> cmakeCacheDir = new HashSet<>();
cmakeCacheDir.add(current);
// Early return to avoid traversing the children.
return cmakeCacheDir;
}
HashSet<WorkspacePath> toIgnore = new HashSet<>();
for (Map.Entry<WorkspacePath, DirectoryStructure> child : ds.directories.entrySet()) {
HashSet<WorkspacePath> toIgnoreInChildren = walkDirectories(child.getKey(), child.getValue(), excludedDirectories, wsRoot);
toIgnore.addAll(toIgnoreInChildren);
}
return toIgnore;
}

@Override
public void afterSync(Project project, BlazeContext context, SyncMode syncMode, SyncResult syncResult, ImmutableSet<Integer> buildIds) {
WorkspaceRoot wsRoot = WorkspaceRoot.fromProject(project);
ProjectViewSet pvs = ProjectViewManager.getInstance(project).reloadProjectView(context);
ImportRoots importRoots = ImportRoots.forProjectSafe(project);
ProgressManager.getInstance().run(new Task.Backgroundable(project, "Detect CMake cache directories", true) {
@Override
public void run(@NotNull ProgressIndicator progressIndicator) {
DirectoryStructure directoryStructure = FutureUtil.waitForFuture(
context,
DirectoryStructure.getRootDirectoryStructure(project, wsRoot, pvs)
).run().result();
if (directoryStructure == null) {
logger.error("Failed to get excluded directories from project view.");
progressIndicator.stop();
return;
}

HashSet<WorkspacePath> directoriesToIgnore = new HashSet<>();
for (WorkspacePath root : importRoots.rootDirectories()) {
directoriesToIgnore.addAll(walkDirectories(root, directoryStructure.directories.get(root), importRoots.excludeDirectories(), wsRoot));
}
if (!directoriesToIgnore.isEmpty()) {
directoriesToIgnore.forEach(dir -> {
markIgnored(project, dir);
});
resync(project);
ExcludeDirectoryInProjectView.notifyProjectViewChanged(project);
}
progressIndicator.stop();
}
});
}
}

private static void markIgnored(Project project, WorkspacePath directoryToIgnore) {
ProjectViewEdit edit = ProjectViewEdit.editLocalProjectView(
project,
builder -> {
ListSection<DirectoryEntry> directorySection = builder.getLast(DirectorySection.KEY);
builder.replace(
directorySection,
ListSection.update(DirectorySection.KEY, directorySection)
.add(DirectoryEntry.exclude(directoryToIgnore))
);
return true;
}
);
if (edit == null) {
Messages.showErrorDialog(
"Could not modify project view. Check for errors in your project view and try again",
"Error");
return;
}
edit.apply();
}

private static void markIgnoredAndResync(Project project, WorkspacePath directoryToIgnore) {
markIgnored(project, directoryToIgnore);
resync(project);
notifyProjectViewChanged(project);
}

private static void resync(Project project) {
BlazeSyncManager.getInstance(project)
.incrementalProjectSync(/* reason= */ "ExcludeDirectoryAction");
}

private static void notifyProjectViewChanged(Project project) {
NotificationGroupManager.getInstance()
.getNotificationGroup("Apple.Bazel.NotificationGroup")
.createNotification("Ignored CMake cache directories", IGNORED_DIRECTORIES_NOTIFICATION_MSG, NotificationType.INFORMATION)
.addAction(NotificationAction.createSimpleExpiring(
"See changes",
() -> OpenProjectViewAction.openLocalProjectViewFile(project)))
.notify(project);
}


public static boolean isCmakeCacheDir(@NotNull File f) {
return f.exists() && f.isDirectory() && f.listFiles((dir, name) -> name.equals(CMAKE_CACHE_DIR_MARKER)).length > 0;
}

public static boolean isCmakeCacheDir(@NotNull VirtualFile vf) {
return vf.exists() && vf.isDirectory() && vf.findChild(CMAKE_CACHE_DIR_MARKER) != null;
}

@NotNull
protected static Optional<VirtualFile> findParentCmakeCacheDir(WorkspaceRoot wsRoot, VirtualFile vf) {
if (vf == null || !vf.exists() || !wsRoot.isInWorkspace(vf)) {
return Optional.empty();
}
if (isCmakeCacheDir(vf)) {
return Optional.of(vf);
}
return findParentCmakeCacheDir(wsRoot, vf.getParent());
}

}
2 changes: 2 additions & 0 deletions base/src/META-INF/blaze-base.xml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
<reference id="Blaze.EditProjectView"/>
<reference id="Blaze.AddDirectoryToProjectView"/>
<reference id="Blaze.AddSourceToProject"/>
<reference id="Apple.Bazel.ExcludeDirectoryInProjectView"/>
</group>
<!--Add popup groups anchored after this bookmark-->
<group id="Blaze.MenuGroupsBookmark"/>
Expand Down Expand Up @@ -194,6 +195,7 @@
<reference ref="Blaze.AddToQuerySyncProjectView"/>
<reference ref="Blaze.OpenCorrespondingBuildFile"/>
<reference ref="Blaze.CopyBlazeTargetPathAction"/>
<reference ref="Apple.Bazel.ExcludeDirectoryInProjectView"/>
</group>
<group id="Internal.Blaze" text="Blaze" popup="true" internal="true">
<action internal="true" id="Blaze.QSync.CleanDependencies" class="com.google.idea.blaze.base.qsync.action.CleanDependencies" text="QSync - Clean Dependencies" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.google.idea.blaze.base.actions;

import com.intellij.ide.projectView.impl.nodes.NamedLibraryElementNode;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.pom.Navigatable;

import javax.annotation.Nullable;

public class ActionMetadataExtractor {
@Nullable
public static NamedLibraryElementNode findLibraryNode(DataContext dataContext) {
Navigatable[] navigatables = CommonDataKeys.NAVIGATABLE_ARRAY.getData(dataContext);
if (navigatables != null && navigatables.length == 1) {
Navigatable navigatable = navigatables[0];
if (navigatable instanceof NamedLibraryElementNode) {
return (NamedLibraryElementNode) navigatable;
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
*/
public class DirectoryStructure {

final ImmutableMap<WorkspacePath, DirectoryStructure> directories;
public final ImmutableMap<WorkspacePath, DirectoryStructure> directories;

private DirectoryStructure(ImmutableMap<WorkspacePath, DirectoryStructure> directories) {
this.directories = directories;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.google.idea.blaze.java.libraries;

import com.google.idea.blaze.base.actions.ActionMetadataExtractor;
import com.google.idea.blaze.base.model.BlazeProjectData;
import com.google.idea.blaze.base.model.LibraryKey;
import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
Expand Down Expand Up @@ -96,7 +97,7 @@ static BlazeJarLibrary findBlazeLibraryForAction(Project project, AnActionEvent
static Library findLibraryForAction(AnActionEvent e) {
Project project = e.getProject();
if (project != null) {
NamedLibraryElementNode node = findLibraryNode(e.getDataContext());
NamedLibraryElementNode node = ActionMetadataExtractor.findLibraryNode(e.getDataContext());
if (node != null) {
String libraryName = node.getName();
if (StringUtil.isNotEmpty(libraryName)) {
Expand All @@ -108,15 +109,4 @@ static Library findLibraryForAction(AnActionEvent e) {
return null;
}

@Nullable
private static NamedLibraryElementNode findLibraryNode(DataContext dataContext) {
Navigatable[] navigatables = CommonDataKeys.NAVIGATABLE_ARRAY.getData(dataContext);
if (navigatables != null && navigatables.length == 1) {
Navigatable navigatable = navigatables[0];
if (navigatable instanceof NamedLibraryElementNode) {
return (NamedLibraryElementNode) navigatable;
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
package com.google.idea.sdkcompat.general;

import com.intellij.openapi.extensions.ExtensionPoint;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.project.Project;
import com.intellij.ui.EditorNotificationPanel;
import com.intellij.ui.EditorNotificationsImpl;
import com.intellij.ui.EditorNotifications;

Expand All @@ -25,4 +27,9 @@ public class EditorNotificationCompat {
public static ExtensionPoint<EditorNotifications.Provider<?>> getEp(Project project) {
return EditorNotificationsImpl.EP_PROJECT.getPoint(project);
}

/** #api222, inline once api222 support is dropped */
public static EditorNotificationPanel Warning(FileEditor editor) {
return new EditorNotificationPanel(editor);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@
package com.google.idea.sdkcompat.general;

import com.intellij.openapi.extensions.ExtensionPoint;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.project.Project;
import com.intellij.ui.EditorNotificationPanel;
import com.intellij.ui.EditorNotificationProvider;

public class EditorNotificationCompat {
/** #api213, inline once api213 support is dropped */
public static ExtensionPoint<EditorNotificationProvider> getEp(Project project) {
return EditorNotificationProvider.EP_NAME.getPoint(project);
}

/** #api222, inline once api222 support is dropped */
public static EditorNotificationPanel Warning(FileEditor editor) {
return new EditorNotificationPanel(editor);
}
}
Loading

0 comments on commit 6e65698

Please sign in to comment.