// Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
// Licensed under the Apache License, Version 2.0 (see LICENSE).
package com.twitter.intellij.pants.components.impl;
import com.intellij.ProjectTopics;
import com.intellij.compiler.server.BuildManagerListener;
import com.intellij.execution.RunManagerAdapter;
import com.intellij.execution.RunManagerEx;
import com.intellij.execution.RunnerAndConfigurationSettings;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.AbstractProjectComponent;
import com.intellij.openapi.externalSystem.ExternalSystemManager;
import com.intellij.openapi.externalSystem.settings.AbstractExternalSystemSettings;
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
import com.intellij.openapi.externalSystem.util.ExternalSystemUtil;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.ModuleListener;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.util.Function;
import com.intellij.util.messages.MessageBusConnection;
import com.twitter.intellij.pants.PantsBundle;
import com.twitter.intellij.pants.components.PantsProjectComponent;
import com.twitter.intellij.pants.execution.PantsMakeBeforeRun;
import com.twitter.intellij.pants.file.FileChangeTracker;
import com.twitter.intellij.pants.metrics.LivePantsMetrics;
import com.twitter.intellij.pants.metrics.PantsExternalMetricsListenerManager;
import com.twitter.intellij.pants.metrics.PantsMetrics;
import com.twitter.intellij.pants.service.project.PantsResolver;
import com.twitter.intellij.pants.settings.PantsProjectSettings;
import com.twitter.intellij.pants.settings.PantsSettings;
import com.twitter.intellij.pants.ui.PantsConsoleManager;
import com.twitter.intellij.pants.util.PantsConstants;
import com.twitter.intellij.pants.util.PantsUtil;
import icons.PantsIcons;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public class PantsProjectComponentImpl extends AbstractProjectComponent implements PantsProjectComponent {
protected PantsProjectComponentImpl(Project project) {
super(project);
}
@Override
public void projectClosed() {
PantsMetrics.report();
FileChangeTracker.unregisterProject(myProject);
PantsConsoleManager.unregisterConsole(myProject);
super.projectClosed();
}
@Override
public void initComponent() {
super.initComponent();
LivePantsMetrics.registerDumbModeListener(myProject);
}
@Override
public void disposeComponent() {
super.disposeComponent();
LivePantsMetrics.unregisterDumbModeListener(myProject);
}
@Override
public void projectOpened() {
PantsMetrics.initialize();
PantsConsoleManager.registerConsole(myProject);
super.projectOpened();
if (myProject.isDefault()) {
return;
}
StartupManager.getInstance(myProject).registerPostStartupActivity(
new Runnable() {
@Override
public void run() {
/**
* Set project to allow dynamic classpath for JUnit run. Still requires any junit run to specify dynamic classpath in
* {@link com.twitter.intellij.pants.execution.PantsClasspathRunConfigurationExtension#updateJavaParameters}
* IDEA's logic: {@link com.intellij.execution.configurations.CommandLineBuilder}
*/
PropertiesComponent.getInstance(myProject).setValue("dynamic.classpath", true);
if (PantsUtil.isSeedPantsProject(myProject)) {
convertToPantsProject();
}
registerExternalBuilderListener();
subscribeToRunConfigurationAddition();
registerFileListener();
final AbstractExternalSystemSettings pantsSettings = ExternalSystemApiUtil.getSettings(myProject, PantsConstants.SYSTEM_ID);
final boolean resolverVersionMismatch =
pantsSettings instanceof PantsSettings && ((PantsSettings) pantsSettings).getResolverVersion() != PantsResolver.VERSION;
if (resolverVersionMismatch && PantsUtil.isPantsProject(myProject)) {
final int answer = Messages.showYesNoDialog(
myProject,
PantsBundle.message("pants.project.generated.with.old.version", myProject.getName()),
PantsBundle.message("pants.name"),
PantsIcons.Icon
);
if (answer == Messages.YES) {
PantsUtil.refreshAllProjects(myProject);
}
}
}
/**
* To convert a seed Pants project to a full bloom pants project:
* 1. Obtain the targets and project_path generated by `pants idea-plugin` from
* workspace file `project.iws` via `PropertiesComponent` API.
* 2. Generate a refresh spec based on the info above.
* 3. Explicitly call {@link PantsUtil#refreshAllProjects}.
*/
private void convertToPantsProject() {
PantsExternalMetricsListenerManager.getInstance().logIsGUIImport(false);
String serializedTargets = PropertiesComponent.getInstance(myProject).getValue("targets");
String projectPath = PropertiesComponent.getInstance(myProject).getValue("project_path");
if (serializedTargets == null || projectPath == null) {
return;
}
/**
* Generate the import spec for the next refresh.
*/
final List<String> targetSpecs = PantsUtil.gson.fromJson(serializedTargets, PantsUtil.TYPE_LIST_STRING);
final boolean loadLibsAndSources = true;
final boolean enableIncrementalImport = false;
final boolean useIdeaProjectJdk = false;
final PantsProjectSettings pantsProjectSettings =
new PantsProjectSettings(targetSpecs, projectPath, loadLibsAndSources, enableIncrementalImport, useIdeaProjectJdk);
/**
* Following procedures in {@link com.intellij.openapi.externalSystem.util.ExternalSystemUtil#refreshProjects}:
* Make sure the setting is injected into the project for refresh.
*/
ExternalSystemManager<?, ?, ?, ?, ?> manager = ExternalSystemApiUtil.getManager(PantsConstants.SYSTEM_ID);
if (manager == null) {
return;
}
AbstractExternalSystemSettings settings = manager.getSettingsProvider().fun(myProject);
settings.setLinkedProjectsSettings(Collections.singleton(pantsProjectSettings));
PantsUtil.refreshAllProjects(myProject);
prepareGuiComponents();
// Subscribe the change of module addition, meaning when the project finishes resolves,
// project SDK should be explicitly set.
final MessageBusConnection connection = myProject.getMessageBus().connect(myProject);
connection.subscribe(
ProjectTopics.MODULES, new ModuleListener() {
@Override
public void moduleAdded(@NotNull Project project, @NotNull Module module) {
ApplicationManager.getApplication().runWriteAction(() -> {
Optional<VirtualFile> pantsExecutable = PantsUtil.findPantsExecutable(project);
if (!pantsExecutable.isPresent()) {
return;
}
Optional<Sdk> sdk = PantsUtil.getDefaultJavaSdk(pantsExecutable.get().getPath());
if (!sdk.isPresent()) {
return;
}
ProjectRootManager.getInstance(project).setProjectSdk(sdk.get());
});
}
@Override
public void beforeModuleRemoved(@NotNull Project project, @NotNull Module module) {
}
@Override
public void moduleRemoved(@NotNull Project project, @NotNull Module module) {
}
@Override
public void modulesRenamed(
@NotNull Project project, @NotNull List<Module> modules, @NotNull Function<Module, String> oldNameProvider
) {
}
}
);
}
/**
* Ensure GUI is set correctly because empty IntelliJ project (seed project in this case)
* does not have these set by default.
* 1. Make sure the project view is opened so view switch will follow.
* 2. Pants tool window is initialized; otherwise no message can be shown when invoking `PantsCompile`.
*/
private void prepareGuiComponents() {
if (!ApplicationManager.getApplication().isUnitTestMode()) {
if (ToolWindowManager.getInstance(myProject).getToolWindow("Project") != null) {
ToolWindowManager.getInstance(myProject).getToolWindow("Project").show(null);
}
ExternalSystemUtil.ensureToolWindowInitialized(myProject, PantsConstants.SYSTEM_ID);
}
}
private void subscribeToRunConfigurationAddition() {
RunManagerEx.getInstanceEx(myProject).addRunManagerListener(
new RunManagerAdapter() {
@Override
public void runConfigurationAdded(@NotNull RunnerAndConfigurationSettings settings) {
super.runConfigurationAdded(settings);
if (!PantsUtil.isPantsProject(myProject) && !PantsUtil.isSeedPantsProject(myProject)) {
return;
}
PantsMakeBeforeRun.replaceDefaultMakeWithPantsMake(myProject, settings);
}
}
);
}
}
);
}
/**
* This registers the listener when IDEA external builder process calls Pants.
*/
private void registerExternalBuilderListener() {
MessageBusConnection connection = myProject.getMessageBus().connect();
BuildManagerListener buildManagerListener = new BuildManagerListener() {
@Override
public void beforeBuildProcessStarted(Project project, UUID sessionId) {
}
@Override
public void buildStarted(Project project, UUID sessionId, boolean isAutomake) {
}
@Override
public void buildFinished(Project project, UUID sessionId, boolean isAutomake) {
/**
* Sync files as generated sources may have changed after external compile,
* specifically when {@link com.twitter.intellij.pants.jps.incremental.PantsTargetBuilder} finishes,
* except this code is run within IDEA core, thus having access to file sync calls.
*/
PantsUtil.synchronizeFiles();
}
};
connection.subscribe(BuildManagerListener.TOPIC, buildManagerListener);
}
private void registerFileListener() {
FileChangeTracker.registerProject(myProject);
}
}