blob: 4d53ff5ca02cb23c87d426bc093d2d5c86ff10df [file] [log] [blame]
/*
* Copyright 2016 The Bazel Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.idea.blaze.base.run;
import com.google.common.base.Strings;
import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
import com.google.idea.blaze.base.model.primitives.Kind;
import com.google.idea.blaze.base.model.primitives.Label;
import com.google.idea.blaze.base.model.primitives.TargetExpression;
import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandler;
import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandlerEditor;
import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandlerProvider;
import com.google.idea.blaze.base.run.confighandler.BlazeUnknownRunConfigurationHandler;
import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
import com.google.idea.blaze.base.settings.Blaze;
import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
import com.google.idea.blaze.base.ui.UiUtil;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.Executor;
import com.intellij.execution.RunManager;
import com.intellij.execution.RunnerIconProvider;
import com.intellij.execution.configurations.ConfigurationFactory;
import com.intellij.execution.configurations.LocatableConfigurationBase;
import com.intellij.execution.configurations.RunConfiguration;
import com.intellij.execution.configurations.RunProfileState;
import com.intellij.execution.configurations.RuntimeConfigurationError;
import com.intellij.execution.configurations.RuntimeConfigurationException;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.options.SettingsEditor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBTextField;
import com.intellij.util.ui.UIUtil;
import javax.annotation.Nullable;
import javax.swing.Box;
import javax.swing.Icon;
import javax.swing.JComponent;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
/** A run configuration which executes Blaze commands. */
public class BlazeCommandRunConfiguration extends LocatableConfigurationBase
implements BlazeRunConfiguration, RunnerIconProvider {
private static final Logger LOG = Logger.getInstance(BlazeCommandRunConfiguration.class);
private static final String HANDLER_ATTR = "handler-id";
private static final String TARGET_TAG = "blaze-target";
private static final String KIND_ATTR = "kind";
// Null for configurations created since restart.
@Nullable private Element externalElementBackup;
// Null when there is no target.
@Nullable private TargetExpression target;
// Null if the target is null, not a Label, or not a known rule.
@Nullable private Kind targetKind;
private BlazeCommandRunConfigurationHandler handler;
// Null if the handler is BlazeUnknownRunConfigurationHandler.
@Nullable private BlazeCommandRunConfigurationHandlerProvider handlerProvider;
public BlazeCommandRunConfiguration(Project project, ConfigurationFactory factory, String name) {
super(project, factory, name);
handler = new BlazeUnknownRunConfigurationHandler(this);
}
/** @return The configuration's {@link BlazeCommandRunConfigurationHandler}. */
@NotNull
public BlazeCommandRunConfigurationHandler getHandler() {
return handler;
}
/**
* Gets the configuration's {@link BlazeCommandRunConfigurationHandler} if it is an instance of
* the given class; otherwise returns null.
*/
@Nullable
public <T extends BlazeCommandRunConfigurationHandler> T getHandlerIfType(Class<T> type) {
if (type.isInstance(handler)) {
return type.cast(handler);
} else {
return null;
}
}
@Override
@Nullable
public TargetExpression getTarget() {
return target;
}
public void setTarget(@Nullable TargetExpression target) {
this.target = target;
RuleIdeInfo rule = getRuleForTarget();
targetKind = rule != null ? rule.kind : null;
BlazeCommandRunConfigurationHandlerProvider handlerProvider =
BlazeCommandRunConfigurationHandlerProvider.findHandlerProvider(targetKind);
setHandlerIfDifferentProvider(handlerProvider);
}
private void setHandlerIfDifferentProvider(
BlazeCommandRunConfigurationHandlerProvider newProvider) {
// Only change the handler if the provider has changed.
if (handlerProvider != newProvider) {
handlerProvider = newProvider;
handler = newProvider.createHandler(this);
}
}
/**
* Returns the single blaze target corresponding to the configuration's target expression, if one
* exists. Returns null if the target expression points to multiple blaze targets, or wasn't
* included in the latest sync.
*/
@Nullable
public RuleIdeInfo getRuleForTarget() {
if (target instanceof Label) {
return RuleFinder.getInstance().ruleForTarget(getProject(), (Label) target);
}
return null;
}
/**
* @return The {@link Kind} name, if the target is a known rule. Otherwise, "target pattern" if it
* is a general {@link TargetExpression}, "unknown rule" if it is a {@link Label} without a
* known rule, and "unknown target" if there is no target.
*/
public String getTargetKindName() {
RuleIdeInfo rule = getRuleForTarget();
if (rule != null) {
return rule.kind.toString();
} else if (target instanceof Label) {
return "unknown rule";
} else if (target != null) {
return "target pattern";
} else {
return "unknown target";
}
}
// TODO This method can be private after BlazeCommandRunConfigurationUpdater is removed.
void loadExternalElementBackup() {
if (externalElementBackup != null) {
try {
handler.readExternal(externalElementBackup);
} catch (InvalidDataException e) {
// This is what IntelliJ does when getting this exception while loading a configuration.
LOG.error(e);
}
}
}
@Override
public void checkConfiguration() throws RuntimeConfigurationException {
// Our handler check and its quick fix are not valid when we don't have BlazeProjectData.
if (BlazeProjectDataManager.getInstance(getProject()).getBlazeProjectData() == null) {
throw new RuntimeConfigurationError(
"Configuration cannot be used or modified while project is syncing.");
}
if (isConfigurationInvalidated()) {
throw new RuntimeConfigurationError(
"A property of the target unexpectedly changed. The configuration must be updated. "
+ "Some configuration settings may be lost.",
() -> {
BlazeCommandRunConfigurationHandler oldHandler = handler;
setTarget(target);
if (handler != oldHandler) {
loadExternalElementBackup();
}
});
}
if (target == null) {
throw new RuntimeConfigurationError(
String.format(
"You must specify a %s target expression.", Blaze.buildSystemName(getProject())));
}
if (!target.toString().startsWith("//")) {
throw new RuntimeConfigurationError(
"You must specify the full target expression, starting with //");
}
handler.checkConfiguration();
}
private boolean isConfigurationInvalidated() {
boolean configurationInvalidated = handler instanceof BlazeUnknownRunConfigurationHandler;
if (!configurationInvalidated) {
RuleIdeInfo rule = getRuleForTarget();
Kind expectedKind = rule != null ? rule.kind : null;
configurationInvalidated = targetKind != expectedKind;
}
if (!configurationInvalidated) {
configurationInvalidated =
handlerProvider
!= BlazeCommandRunConfigurationHandlerProvider.findHandlerProvider(targetKind);
}
return configurationInvalidated;
}
@Override
public void readExternal(Element element) throws InvalidDataException {
super.readExternal(element);
externalElementBackup = element.clone();
// Target is persisted as a tag to permit multiple targets in the future.
Element targetElement = element.getChild(TARGET_TAG);
if (targetElement != null && !Strings.isNullOrEmpty(targetElement.getTextTrim())) {
target = TargetExpression.fromString(targetElement.getTextTrim());
targetKind = Kind.fromString(targetElement.getAttributeValue(KIND_ATTR));
} else {
// Legacy: Added in 1.9 to support reading target as an attribute so
// BlazeAndroid(Binary/Test)RunConfiguration elements can be read.
// TODO remove in 2.1 once BlazeAndroidBinaryRunConfigurationType and
// BlazeAndroidTestRunConfigurationType have been removed.
String targetString =
element.getAttributeValue(
TARGET_TAG); // The attribute ID happens to be identical to the tag ID.
if (targetString != null) {
target = TargetExpression.fromString(targetString);
// Once the above is removed, 'target = null;' should be
// the only thing in the outer else clause.
} else {
target = null;
}
}
// Because BlazeProjectData is not available when configurations are loading,
// we can't call setTarget and have it find the appropriate handler provider.
// So instead, we use the stored provider ID.
String providerId = element.getAttributeValue(HANDLER_ATTR);
BlazeCommandRunConfigurationHandlerProvider handlerProvider =
BlazeCommandRunConfigurationHandlerProvider.getHandlerProvider(providerId);
if (handlerProvider != null) {
setHandlerIfDifferentProvider(handlerProvider);
}
handler.readExternal(element);
}
@Override
@SuppressWarnings("ThrowsUncheckedException")
public void writeExternal(Element element) throws WriteExternalException {
super.writeExternal(element);
// We can't write externalElementBackup contents; doing so would cause the configuration
// xml to retain duplicate elements and grow across reopenings.
// We also can't use the approach in BlazeUnknownRunConfigurationHandler;
// this can revive intentionally deleted attributes/elements such as user flags.
if (target != null) {
// Target is persisted as a tag to permit multiple targets in the future.
Element targetElement = new Element(TARGET_TAG);
targetElement.setText(target.toString());
if (targetKind != null) {
targetElement.setAttribute(KIND_ATTR, targetKind.toString());
}
element.addContent(targetElement);
}
if (handlerProvider != null) {
element.setAttribute(HANDLER_ATTR, handlerProvider.getId());
}
handler.writeExternal(element);
}
@Override
public BlazeCommandRunConfiguration clone() {
final BlazeCommandRunConfiguration configuration = (BlazeCommandRunConfiguration) super.clone();
if (externalElementBackup != null) {
configuration.externalElementBackup = externalElementBackup.clone();
}
configuration.target = target;
configuration.targetKind = targetKind;
configuration.handler = handler.cloneFor(configuration);
configuration.handlerProvider = handlerProvider;
return configuration;
}
@Override
@Nullable
public RunProfileState getState(
@NotNull Executor executor, @NotNull ExecutionEnvironment environment)
throws ExecutionException {
return handler.getState(executor, environment);
}
@Override
@Nullable
public String suggestedName() {
return handler.suggestedName();
}
@Override
public boolean isGeneratedName() {
return handler.isGeneratedName(super.isGeneratedName());
}
@Override
@Nullable
public Icon getExecutorIcon(@NotNull RunConfiguration configuration, @NotNull Executor executor) {
return handler.getExecutorIcon(configuration, executor);
}
@Override
@NotNull
public SettingsEditor<? extends BlazeCommandRunConfiguration> getConfigurationEditor() {
return new BlazeCommandRunConfigurationSettingsEditor(this);
}
static class BlazeCommandRunConfigurationSettingsEditor
extends SettingsEditor<BlazeCommandRunConfiguration> {
@Nullable private BlazeCommandRunConfigurationHandlerProvider handlerProvider;
private BlazeCommandRunConfigurationHandlerEditor handlerEditor;
@Nullable private JComponent handlerComponent;
private final Box editor;
private final JBLabel targetExpressionLabel;
private final JBTextField targetField = new JBTextField(1);
private boolean isEditable;
public BlazeCommandRunConfigurationSettingsEditor(BlazeCommandRunConfiguration config) {
targetExpressionLabel = new JBLabel(UIUtil.ComponentStyle.LARGE);
editor = UiUtil.createBox(targetExpressionLabel, targetField);
targetField.getEmptyText().setText("Full target expression starting with //");
updateTargetExpressionLabel(config);
updateHandlerEditor(config);
setEditable(isConfigurationEditable(config));
}
private static boolean isConfigurationEditable(BlazeCommandRunConfiguration config) {
RunConfiguration template =
RunManager.getInstance(config.getProject())
.getConfigurationTemplate(config.getFactory())
.getConfiguration();
if (config == template) {
return true; // The default template is always editable.
}
return BlazeProjectDataManager.getInstance(config.getProject()).getBlazeProjectData() != null
&& !config.isConfigurationInvalidated();
}
private void setEditable(boolean editable) {
isEditable = editable;
targetField.setEnabled(isEditable);
if (handlerComponent != null) {
handlerComponent.setVisible(isEditable);
}
}
private void updateTargetExpressionLabel(BlazeCommandRunConfiguration config) {
targetExpressionLabel.setText(
String.format(
"Target expression (%s handled by %s):",
config.getTargetKindName(), config.handler.getHandlerName()));
}
private void updateHandlerEditor(BlazeCommandRunConfiguration config) {
handlerProvider = config.handlerProvider;
handlerEditor = config.handler.getHandlerEditor();
if (handlerComponent != null) {
editor.remove(handlerComponent);
}
handlerComponent = handlerEditor.createEditor();
if (handlerComponent != null) {
editor.add(handlerComponent);
}
}
@Override
@NotNull
protected JComponent createEditor() {
return editor;
}
@Override
protected void resetEditorFrom(BlazeCommandRunConfiguration config) {
updateTargetExpressionLabel(config);
if (config.handlerProvider != handlerProvider) {
updateHandlerEditor(config);
}
setEditable(isConfigurationEditable(config));
targetField.setText(config.target == null ? null : config.target.toString());
handlerEditor.resetEditorFrom(config.handler);
}
@Override
protected void applyEditorTo(BlazeCommandRunConfiguration config) {
if (!isEditable) {
return;
}
applyTarget(config);
updateTargetExpressionLabel(config);
if (config.handlerProvider != handlerProvider) {
updateHandlerEditor(config);
handlerEditor.resetEditorFrom(config.handler);
} else {
handlerEditor.applyEditorTo(config.handler);
}
}
private void applyTarget(BlazeCommandRunConfiguration config) {
String targetString = targetField.getText();
BlazeCommandRunConfigurationHandler oldHandler = config.handler;
config.setTarget(
Strings.isNullOrEmpty(targetString) ? null : TargetExpression.fromString(targetString));
if (config.handler != oldHandler) {
config.loadExternalElementBackup();
}
}
}
}