// Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
// Licensed under the Apache License, Version 2.0 (see LICENSE).
package com.twitter.intellij.pants.service.project;
import com.intellij.ProjectTopics;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessOutputTypes;
import com.intellij.ide.FileSelectInContext;
import com.intellij.ide.SelectInContext;
import com.intellij.ide.SelectInTarget;
import com.intellij.ide.projectView.ProjectView;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.externalSystem.model.DataNode;
import com.intellij.openapi.externalSystem.model.ExternalSystemException;
import com.intellij.openapi.externalSystem.model.ProjectKeys;
import com.intellij.openapi.externalSystem.model.project.ContentRootData;
import com.intellij.openapi.externalSystem.model.project.ExternalSystemSourceType;
import com.intellij.openapi.externalSystem.model.project.ModuleData;
import com.intellij.openapi.externalSystem.model.project.ProjectData;
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId;
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationEvent;
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListener;
import com.intellij.openapi.externalSystem.service.project.ExternalSystemProjectResolver;
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.module.ModuleTypeId;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModuleRootAdapter;
import com.intellij.openapi.roots.ModuleRootEvent;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowId;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.util.Consumer;
import com.intellij.util.messages.MessageBusConnection;
import com.twitter.intellij.pants.metrics.PantsExternalMetricsListenerManager;
import com.twitter.intellij.pants.projectview.PantsProjectPaneSelectInTarget;
import com.twitter.intellij.pants.projectview.ProjectFilesViewPane;
import com.twitter.intellij.pants.service.PantsCompileOptionsExecutor;
import com.twitter.intellij.pants.settings.PantsExecutionSettings;
import com.twitter.intellij.pants.util.PantsConstants;
import com.twitter.intellij.pants.util.PantsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class PantsSystemProjectResolver implements ExternalSystemProjectResolver<PantsExecutionSettings> {
protected static final Logger LOG = Logger.getInstance(PantsSystemProjectResolver.class);
private final Map<ExternalSystemTaskId, PantsCompileOptionsExecutor> task2executor =
new ConcurrentHashMap<ExternalSystemTaskId, PantsCompileOptionsExecutor>();
@Nullable
@Override
public DataNode<ProjectData> resolveProjectInfo(
@NotNull ExternalSystemTaskId id,
@NotNull String projectPath,
boolean isPreviewMode,
@Nullable PantsExecutionSettings settings,
@NotNull ExternalSystemTaskNotificationListener listener
) throws ExternalSystemException, IllegalArgumentException, IllegalStateException {
if (projectPath.startsWith(".pants.d")) {
return null;
}
checkForDifferentPantsExecutables(id, projectPath);
final PantsCompileOptionsExecutor executor = PantsCompileOptionsExecutor.create(projectPath, settings);
task2executor.put(id, executor);
final DataNode<ProjectData> projectDataNode =
resolveProjectInfoImpl(id, executor, listener, isPreviewMode, settings.isEnableIncrementalImport());
doViewSwitch(id, projectPath);
task2executor.remove(id);
return projectDataNode;
}
private void doViewSwitch(@NotNull ExternalSystemTaskId id, @NotNull String projectPath) {
Project ideProject = id.findProject();
if (ideProject == null) {
return;
}
// Disable zooming on subsequent project resolves/refreshes,
// i.e. a project that already has existing modules, because it may zoom at a module
// that is going to be replaced by the current resolve.
if (ModuleManager.getInstance(ideProject).getModules().length > 0) {
return;
}
MessageBusConnection messageBusConnection = ideProject.getMessageBus().connect();
messageBusConnection.subscribe(
ProjectTopics.PROJECT_ROOTS,
new ModuleRootAdapter() {
@Override
public void rootsChanged(ModuleRootEvent event) {
// Initiate view switch only when project modules have been created.
new ViewSwitchProcessor(ideProject, projectPath).asyncViewSwitch();
}
}
);
}
/**
* Check whether the pants executable of the new project to import is the same as the existing project's pants executable.
*/
private void checkForDifferentPantsExecutables(@NotNull ExternalSystemTaskId id, @NotNull String projectPath) {
final Project existingIdeProject = id.findProject();
if (existingIdeProject == null) {
return;
}
String projectFilePath = existingIdeProject.getProjectFilePath();
if (projectFilePath == null) {
return;
}
final Optional<VirtualFile> existingPantsExe = PantsUtil.findPantsExecutable(projectFilePath);
final Optional<VirtualFile> newPantsExe = PantsUtil.findPantsExecutable(projectPath);
if (!existingPantsExe.isPresent() || !newPantsExe.isPresent()) {
return;
}
if (!existingPantsExe.get().getCanonicalFile().getPath().equals(newPantsExe.get().getCanonicalFile().getPath())) {
throw new ExternalSystemException(String.format(
"Failed to import. Target/Directory to be added uses a different pants executable %s compared to the existing project's %s",
existingPantsExe, newPantsExe
));
}
}
@NotNull
private DataNode<ProjectData> resolveProjectInfoImpl(
@NotNull ExternalSystemTaskId id,
@NotNull final PantsCompileOptionsExecutor executor,
@NotNull ExternalSystemTaskNotificationListener listener,
boolean isPreviewMode,
boolean isEnableImcrementalImport
) throws ExternalSystemException, IllegalArgumentException, IllegalStateException {
// todo(fkorotkov): add ability to choose a name for a project
final ProjectData projectData = new ProjectData(
PantsConstants.SYSTEM_ID,
executor.getProjectName(),
executor.getBuildRoot().getPath() + "/.idea/pants-projects/" + executor.getProjectRelativePath(),
executor.getProjectPath()
);
final DataNode<ProjectData> projectDataNode = new DataNode<ProjectData>(ProjectKeys.PROJECT, projectData, null);
PantsUtil.findPantsExecutable(executor.getProjectPath())
.flatMap(file -> PantsUtil.getDefaultJavaSdk(file.getPath()))
.ifPresent(sdk -> projectDataNode.createChild(PantsConstants.SDK_KEY, sdk));
if (!isPreviewMode) {
PantsExternalMetricsListenerManager.getInstance().logIsIncrementalImport(isEnableImcrementalImport);
resolveUsingPantsGoal(id, executor, listener, projectDataNode);
if (!containsContentRoot(projectDataNode, executor.getProjectDir())) {
// Add a module with content root as import project directory path.
// This will allow all the files in the imported project directory will be indexed by the plugin.
final String moduleName = executor.getRootModuleName();
final ModuleData moduleData = new ModuleData(
PantsConstants.PANTS_PROJECT_MODULE_ID_PREFIX + moduleName,
PantsConstants.SYSTEM_ID,
ModuleTypeId.JAVA_MODULE,
moduleName + PantsConstants.PANTS_PROJECT_MODULE_SUFFIX,
projectData.getIdeProjectFileDirectoryPath() + "/" + moduleName,
executor.getProjectPath()
);
final DataNode<ModuleData> moduleDataNode = projectDataNode.createChild(ProjectKeys.MODULE, moduleData);
final ContentRootData contentRoot = new ContentRootData(PantsConstants.SYSTEM_ID, executor.getProjectDir());
if (FileUtil.filesEqual(executor.getBuildRoot(), new File(executor.getProjectPath()))) {
contentRoot.storePath(ExternalSystemSourceType.EXCLUDED, executor.getBuildRoot().getPath() + "/.idea");
}
moduleDataNode.createChild(ProjectKeys.CONTENT_ROOT, contentRoot);
}
}
return projectDataNode;
}
private boolean containsContentRoot(@NotNull DataNode<ProjectData> projectDataNode, @NotNull String path) {
for (DataNode<ModuleData> moduleDataNode : ExternalSystemApiUtil.findAll(projectDataNode, ProjectKeys.MODULE)) {
for (DataNode<ContentRootData> contentRootDataNode : ExternalSystemApiUtil.findAll(moduleDataNode, ProjectKeys.CONTENT_ROOT)) {
final ContentRootData contentRootData = contentRootDataNode.getData();
if (FileUtil.isAncestor(contentRootData.getRootPath(), path, false)) {
return true;
}
}
}
return false;
}
private void resolveUsingPantsGoal(
@NotNull final ExternalSystemTaskId id,
@NotNull PantsCompileOptionsExecutor executor,
final ExternalSystemTaskNotificationListener listener,
@NotNull DataNode<ProjectData> projectDataNode
) {
final PantsResolver dependenciesResolver = new PantsResolver(executor);
dependenciesResolver.resolve(
new Consumer<String>() {
@Override
public void consume(String status) {
listener.onStatusChange(new ExternalSystemTaskNotificationEvent(id, status));
}
},
new ProcessAdapter() {
@Override
public void onTextAvailable(ProcessEvent event, Key outputType) {
listener.onTaskOutput(id, event.getText(), outputType == ProcessOutputTypes.STDOUT);
}
}
);
dependenciesResolver.addInfoTo(projectDataNode);
}
@Override
public boolean cancelTask(@NotNull ExternalSystemTaskId taskId, @NotNull ExternalSystemTaskNotificationListener listener) {
final PantsCompileOptionsExecutor executor = task2executor.remove(taskId);
return executor != null && executor.cancelAllProcesses();
}
private class ViewSwitchProcessor {
private final Project myProject;
private final String myProjectPath;
private ScheduledFuture<?> directoryFocusHandle;
public ViewSwitchProcessor(final Project project, final String projectPath) {
myProject = project;
myProjectPath = projectPath;
}
public void asyncViewSwitch() {
/**
* Make sure the project view opened so the view switch will follow.
*/
final ToolWindow projectWindow = ToolWindowManager.getInstance(myProject).getToolWindow(ToolWindowId.PROJECT_VIEW);
if (projectWindow == null) {
return;
}
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
// Show Project Pane, and switch to ProjectFilesViewPane right after.
projectWindow.show(new Runnable() {
@Override
public void run() {
ProjectView.getInstance(myProject).changeView(ProjectFilesViewPane.ID);
queueFocusOnImportDirectory();
}
});
}
});
}
private void queueFocusOnImportDirectory() {
directoryFocusHandle = PantsUtil.scheduledThreadPool.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
if (ModuleManager.getInstance(myProject).getModules().length == 0 ||
!ProjectView.getInstance(myProject).getCurrentViewId().equals(ProjectFilesViewPane.ID)) {
return;
}
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
final VirtualFile pathImported = VirtualFileManager.getInstance().findFileByUrl("file://" + myProjectPath);
// Skip focusing if directory is not found.
if (pathImported != null) {
VirtualFile importDirectory = pathImported.isDirectory() ? pathImported : pathImported.getParent();
SelectInContext selectInContext = new FileSelectInContext(myProject, importDirectory);
for (SelectInTarget selectInTarget : ProjectView.getInstance(myProject).getSelectInTargets()) {
if (selectInTarget instanceof PantsProjectPaneSelectInTarget) {
selectInTarget.selectIn(selectInContext, false);
break;
}
}
}
final boolean mayInterruptIfRunning = true;
directoryFocusHandle.cancel(mayInterruptIfRunning);
}
});
}
}, 0, 1, TimeUnit.SECONDS);
}
}
}