/*
* Copyright 2003-2016 JetBrains s.r.o.
*
* 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 jetbrains.mps.ide.typesystem.trace;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.ui.JBColor;
import jetbrains.mps.ide.ui.tree.MPSTree;
import jetbrains.mps.ide.ui.tree.MPSTreeNode;
import jetbrains.mps.ide.ui.tree.TextTreeNode;
import jetbrains.mps.ide.util.ColorAndGraphicsUtil;
import jetbrains.mps.lang.smodel.generator.smodelAdapter.SNodeOperations;
import jetbrains.mps.newTypesystem.TypesUtil;
import jetbrains.mps.newTypesystem.operation.AbstractOperation;
import jetbrains.mps.newTypesystem.operation.AddErrorOperation;
import jetbrains.mps.newTypesystem.operation.ApplyRuleOperation;
import jetbrains.mps.newTypesystem.operation.AssignTypeOperation;
import jetbrains.mps.newTypesystem.operation.ExpandTypeOperation;
import jetbrains.mps.newTypesystem.operation.TraceWarningOperation;
import jetbrains.mps.newTypesystem.operation.block.AbstractBlockOperation;
import jetbrains.mps.newTypesystem.operation.block.AddDependencyOperation;
import jetbrains.mps.newTypesystem.operation.block.RemoveDependencyOperation;
import jetbrains.mps.newTypesystem.operation.equation.AddEquationOperation;
import jetbrains.mps.newTypesystem.state.State;
import jetbrains.mps.newTypesystem.state.blocks.Block;
import jetbrains.mps.nodeEditor.DefaultEditorMessage;
import jetbrains.mps.nodeEditor.EditorComponent;
import jetbrains.mps.nodeEditor.NodeHighlightManager;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.message.EditorMessageOwner;
import jetbrains.mps.openapi.editor.message.SimpleEditorMessage;
import jetbrains.mps.openapi.editor.style.StyleRegistry;
import jetbrains.mps.project.Project;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.util.Computable;
import jetbrains.mps.util.Pair;
import jetbrains.mps.workbench.MPSDataKeys;
import jetbrains.mps.workbench.action.ActionUtils;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.model.SNode;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import java.awt.Color;
import java.awt.Graphics;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
public class TypeSystemTraceTree extends MPSTree implements DataProvider {
private final Project myProject;
private final TypecheckingContextTracker myContextTracker;
private final SNode mySelectedNode;
private Set<SNode> myNodes;
private TypeSystemTracePanel myParent;
private EditorComponent myEditorComponent;
private List<TypeSystemTraceTreeNode> myErrorNodes = new LinkedList<TypeSystemTraceTreeNode>();
private TypeSystemTraceTree.DetailsTree myDetailsTree;
private final NodeHighlightManager myHighlightManager;
private EditorMessageOwner myMessageOwner;
public TypeSystemTraceTree(Project mpsProject, SNode node, TypeSystemTracePanel parent, EditorComponent editorComponent) {
myProject = mpsProject;
myContextTracker = new TypecheckingContextTracker(node.getContainingRoot());
myParent = parent;
myEditorComponent = editorComponent;
mySelectedNode = node;
initNodes(node);
setGenerationMode(TraceSettings.isGenerationMode());
this.myHighlightManager = editorComponent.getHighlightManager();
this.myMessageOwner = new EditorMessageOwner() {
};
EditorCell nodeCell = myEditorComponent.findNodeCell(mySelectedNode);
if (nodeCell != null) {
myHighlightManager.mark(new TypeSystemTraceTree.SelectedNodeEditorMessage(nodeCell, ""));
}
this.rebuildNow();
expandAll();
this.myDetailsTree = new TypeSystemTraceTree.DetailsTree(null);
addTreeSelectionListener(new TypeSystemTraceTree.ShowDetailsUpdater());
getSelectionModel().setSelectionMode(TreeSelectionModel.CONTIGUOUS_TREE_SELECTION);
}
private void initNodes(SNode node) {
myNodes = new HashSet<SNode>();
myNodes.addAll(SNodeOperations.getNodeDescendants(node, null, false, new SConcept[]{}));
myNodes.add(node);
}
/*package*/ MPSTree getDetailsTree() {
return myDetailsTree;
}
public void rebuildTrace() {
myContextTracker.checkRoot(true);
this.rebuildNow();
this.expandAll();
}
public void setGenerationMode(boolean generationMode) {
myContextTracker.setGenerationMode(generationMode, mySelectedNode);
}
public State getState() {
return myContextTracker.getStateCopy();
}
@Override
protected MPSTreeNode rebuild() {
setRootVisible(false);
setGenerationMode(TraceSettings.isGenerationMode());
if (TraceSettings.isTraceForSelectedNode() && mySelectedNode != null) {
getSliceVars(myContextTracker.getOperation());
}
MPSTreeNode result = create(myContextTracker.getOperation(), true);
if (result == null) {
result = new TextTreeNode("Empty type system trace");
}
// <node>
return result;
}
public TypeSystemTraceTreeNode create(AbstractOperation operation, boolean withChildren) {
if (!(filterNodeType(operation))) {
return null;
}
final boolean showNode = showNodeRecursively(operation);
List<TypeSystemTraceTreeNode> children = new ArrayList<TypeSystemTraceTreeNode>();
if (withChildren) {
for (AbstractOperation consequence : operation.getConsequences()) {
TypeSystemTraceTreeNode node = create(consequence, false);
if (node != null) {
children.add(node);
}
}
} else if (!(showNode)) {
if (!(operation.getConsequences().iterator().hasNext())) {
return null;
}
}
final boolean hasAnError = hasAnErrorAsConsequence(operation);
TypeSystemTraceTreeNode result = new TypeSystemTraceTreeNode(operation, myContextTracker.getCurrentState(), myEditorComponent) {
@Override
public void doUpdatePresentation() {
super.doUpdatePresentation();
if (!(showNode)) {
setColor(JBColor.GRAY);
} else if (hasAnError) {
setColor(JBColor.RED);
}
}
};
for (TypeSystemTraceTreeNode node : children) {
result.add(node);
}
if (hasAnError) {
myErrorNodes.add(result);
}
return result;
}
private boolean hasAnErrorAsConsequence(AbstractOperation operation) {
if (operation instanceof AddErrorOperation || operation instanceof TraceWarningOperation) {
return true;
}
for (AbstractOperation consequence : operation.getConsequences()) {
if (hasAnErrorAsConsequence(consequence)) {
return true;
}
}
return false;
}
@Override
public void dispose() {
myHighlightManager.clearForOwner(myMessageOwner);
myContextTracker.dispose();
super.dispose();
}
private boolean showNode(AbstractOperation diff) {
if (!(TraceSettings.isTraceForSelectedNode())) {
return true;
}
if (mySelectedNode == null) {
return true;
}
if (myNodes.contains(diff.getSource())) {
return true;
}
if (diff instanceof AddEquationOperation) {
AddEquationOperation eq = (AddEquationOperation) diff;
if (myNodes.contains(eq.getChild()) || myNodes.contains(eq.getParent())) {
return true;
}
}
if (diff instanceof AbstractBlockOperation) {
Block block = ((AbstractBlockOperation) diff).getBlock();
for (SNode node : block.getInputs()) {
if (myNodes.contains(node)) {
return true;
}
}
}
return false;
}
private boolean showNodeRecursively(AbstractOperation diff) {
if (diff == null) {
return false;
}
for (AbstractOperation csq : diff.getConsequences()) {
if (showNodeRecursively(csq)) {
return true;
}
}
return showNode(diff);
}
private boolean filterNodeType(AbstractOperation operation) {
if (!(TraceSettings.isShowTypesExpansion()) && operation instanceof ExpandTypeOperation) {
return false;
}
if (!(TraceSettings.isShowApplyRuleOperations()) && operation instanceof ApplyRuleOperation) {
return false;
}
if (!(TraceSettings.isShowBlockDependencies()) && (operation instanceof AddDependencyOperation || operation instanceof RemoveDependencyOperation)) {
return false;
}
return true;
}
public void goToNextError() {
int currentRow = -1;
MPSTreeNode currentNode = this.getCurrentNode();
if (null != currentNode) {
currentRow = this.getRowForPath(new TreePath(currentNode.getPath()));
}
MPSTreeNode errorNode = getNextErrorNode(currentRow);
if (null != errorNode) {
this.scrollPathToVisible(new TreePath(errorNode.getPath()));
this.selectNode(errorNode);
}
}
private MPSTreeNode getNextErrorNode(int row) {
TreePath errorPath;
int errorRow;
for (TypeSystemTraceTreeNode errorNode : myErrorNodes) {
errorPath = new TreePath(errorNode.getPath());
errorRow = this.getRowForPath(errorPath);
if (errorRow > row) {
return errorNode;
}
}
if (!(myErrorNodes.isEmpty())) {
return myErrorNodes.get(0);
}
return null;
}
private void getSliceVars(AbstractOperation diff) {
if (diff == null) {
return;
}
if (diff instanceof AddEquationOperation) {
AddEquationOperation eq = (AddEquationOperation) diff;
SNode child = eq.getChild();
SNode parent = eq.getParent();
if (myNodes.contains(child)) {
myNodes.addAll(TypesUtil.getVariables(parent, myContextTracker.getStateCopy()));
}
if (myNodes.contains(parent)) {
myNodes.addAll(TypesUtil.getVariables(child, myContextTracker.getStateCopy()));
}
}
if (diff instanceof AssignTypeOperation) {
AssignTypeOperation typeDifference = (AssignTypeOperation) diff;
if (myNodes.contains(typeDifference.getNode()) && TypesUtil.isVariable(typeDifference.getType())) {
myNodes.add(typeDifference.getType());
}
}
for (AbstractOperation childDiff : diff.getConsequences()) {
getSliceVars(childDiff);
}
}
@Override
@Nullable
public Object getData(@NonNls final String id) {
MPSTreeNode currentNode = this.getCurrentNode();
if (currentNode instanceof TypeSystemTraceTreeNode) {
return new ModelAccessHelper(myProject.getModelAccess()).runReadAction(new Computable<Object>() {
@Override
public Object compute() {
return _getData(id);
}
});
}
return null;
}
private Object _getData(String id) {
AbstractOperation operation = getAssociatedOperation(this.getCurrentNode());
if (operation == null) {
return null;
}
final Pair<String, String> rule = operation.getRule();
final SNode source = operation.getSource();
if (id.equals(MPSDataKeys.RULE_MODEL_AND_ID.getName())) {
return rule;
}
if (source != null && source.getModel() != null) {
if (id.equals(MPSDataKeys.SOURCE_NODE.getName())) {
return source;
}
}
return null;
}
@Override
protected ActionGroup createPopupActionGroup(final MPSTreeNode treeNode) {
return ActionUtils.groupFromActions(ActionManager.getInstance().getAction("jetbrains.mps.ide.actions.GoToNode_Action"));
}
private void showState(final TypeSystemTraceTreeNode newNode) {
myProject.getModelAccess().runReadAction(new Runnable() {
@Override
public void run() {
Object difference = newNode.getUserObject();
myParent.resetState(myContextTracker.resetCurrentState((AbstractOperation) difference));
final State newState = myContextTracker.updateCurrentState((AbstractOperation) difference);
if (newState != null) {
myParent.updateState(newState);
}
}
});
}
private void showState(final MPSTreeNode fromNode, final MPSTreeNode toNode) {
myProject.getModelAccess().runReadAction(new Runnable() {
@Override
public void run() {
Object fromDiff = fromNode.getUserObject();
Object toDiff = toNode.getUserObject();
myParent.resetState(myContextTracker.resetCurrentState((AbstractOperation) fromDiff));
myParent.updateState(myContextTracker.updateCurrentState((AbstractOperation) fromDiff, (AbstractOperation) toDiff));
}
});
}
private void showDetails(MPSTreeNode treeNode) {
myDetailsTree.setOperation(getAssociatedOperation(treeNode));
}
private void showDetails(Collection<? extends MPSTreeNode> treeNodes) {
List<AbstractOperation> operations = new ArrayList<AbstractOperation>();
for (MPSTreeNode treeNode : treeNodes) {
operations.add(getAssociatedOperation(treeNode));
}
myDetailsTree.setOperations(operations);
}
private class ShowDetailsUpdater implements TreeSelectionListener {
private ShowDetailsUpdater() {
}
@Override
public void valueChanged(TreeSelectionEvent e) {
TreePath[] selectionPaths = getSelectionPaths();
if (selectionPaths != null && selectionPaths.length >= 1) {
if (selectionPaths.length == 1) {
TreePath path = selectionPaths[0];
if (path == null) {
return;
}
MPSTreeNode treeNode = (MPSTreeNode) path.getLastPathComponent();
showState((TypeSystemTraceTreeNode) treeNode);
showDetails(treeNode);
} else {
TreePath fromPath = selectionPaths[0];
TreePath toPath = selectionPaths[selectionPaths.length - 1];
showState((MPSTreeNode) fromPath.getLastPathComponent(), (MPSTreeNode) toPath.getLastPathComponent());
List<MPSTreeNode> selectedNodes = new ArrayList<MPSTreeNode>();
for (TreePath tp : selectionPaths) {
selectedNodes.add((MPSTreeNode) tp.getLastPathComponent());
}
showDetails(selectedNodes);
}
}
}
}
private class SelectedNodeEditorMessage extends DefaultEditorMessage {
private EditorCell myCell;
public SelectedNodeEditorMessage(EditorCell cell, String message) {
super(cell.getSNode(), StyleRegistry.getInstance().getSimpleColor(new Color(192, 255, 255)), message, myMessageOwner);
this.myCell = cell;
}
@Override
public EditorCell getCell(EditorComponent component) {
return myCell;
}
@Override
public boolean acceptCell(EditorCell cell, EditorComponent component) {
return myCell == cell;
}
@Override
public boolean sameAs(SimpleEditorMessage that) {
return super.sameAs(that) && this.equals(that);
}
@Override
protected void paintWithColor(Graphics graphics, EditorCell cell, Color color) {
int x = cell.getX() + cell.getLeftInset();
int y = cell.getY() + cell.getTopInset();
int width = cell.getWidth() - cell.getLeftInset() - cell.getRightInset() - 1;
int height = cell.getHeight() - cell.getTopInset() - cell.getBottomInset() - 1;
graphics.setColor(color);
ColorAndGraphicsUtil.fillStripes(graphics, x, y, width, height);
}
@Override
public boolean isBackground() {
return true;
}
@Override
public boolean equals(Object that) {
if (that == null) {
return false;
}
if (this == that) {
return true;
}
if (that.getClass() != TypeSystemTraceTree.SelectedNodeEditorMessage.class) {
return false;
}
return this.myCell.equals(((TypeSystemTraceTree.SelectedNodeEditorMessage) that).myCell);
}
@Override
public int hashCode() {
return myCell.hashCode() * 37;
}
}
public class DetailsTree extends MPSTree implements DataProvider {
private List<AbstractOperation> myOperations;
public DetailsTree(AbstractOperation operation) {
this.myOperations = Collections.singletonList(operation);
rebuildNow();
expandAll();
addTreeSelectionListener(new TypeSystemTraceTree.DetailsTree.ShowStateUpdater());
}
public void setOperation(AbstractOperation operation) {
this.myOperations = Collections.singletonList(operation);
rebuildNow();
expandAll();
}
public void setOperations(Collection<? extends AbstractOperation> operation) {
this.myOperations = new ArrayList<AbstractOperation>(operation);
rebuildNow();
expandAll();
}
public Collection<MPSTreeNode> create(Collection<? extends AbstractOperation> operations, boolean showParent) {
if (operations == null || operations.size() == 0 || operations.iterator().next() == null) {
return null;
}
List<MPSTreeNode> result = new ArrayList<MPSTreeNode>();
for (AbstractOperation operation : operations) {
final boolean showNode = showNode(operation);
List<MPSTreeNode> children = new ArrayList<MPSTreeNode>();
for (AbstractOperation consequence : operation.getConsequences()) {
Collection<MPSTreeNode> nodes = create(Collections.singletonList(consequence), true);
if (nodes != null) {
children.addAll(nodes);
}
}
if (!(filterNodeType(operation))) {
continue;
}
if (showParent) {
TypeSystemTraceTreeNode treeNode = new TypeSystemTraceTreeNode(operation, myContextTracker.getCurrentState(), myEditorComponent) {
@Override
public void doUpdatePresentation() {
super.doUpdatePresentation();
if (!(showNode)) {
setColor(JBColor.BLACK);
}
}
};
for (MPSTreeNode node : children) {
treeNode.add(node);
}
result.add(treeNode);
} else {
result.addAll(children);
}
// <node>
}
return result;
}
@Override
@Nullable
public Object getData(@NonNls final String id) {
MPSTreeNode currentNode = this.getCurrentNode();
if (currentNode instanceof TypeSystemTraceTreeNode) {
return new ModelAccessHelper(myProject.getModelAccess()).runReadAction(new Computable<Object>() {
@Override
public Object compute() {
return _getData(id);
}
});
}
return null;
}
private Object _getData(String id) {
AbstractOperation operation = getAssociatedOperation(this.getCurrentNode());
if (operation == null) {
return null;
}
final Pair<String, String> rule = operation.getRule();
final SNode source = operation.getSource();
if (id.equals(MPSDataKeys.RULE_MODEL_AND_ID.getName())) {
return rule;
}
if (source != null && source.getModel() != null) {
if (id.equals(MPSDataKeys.SOURCE_NODE.getName())) {
return source;
}
}
return null;
}
@Override
protected ActionGroup createPopupActionGroup(MPSTreeNode node) {
return ActionUtils.groupFromActions(ActionManager.getInstance().getAction("jetbrains.mps.ide.actions.GoToNode_Action"),
ActionManager.getInstance().getAction("jetbrains.mps.ide.actions.GoToRule_Action"));
}
@Override
protected MPSTreeNode rebuild() {
setRootVisible(false);
MPSTreeNode result;
Collection<MPSTreeNode> nodes = create(myOperations, false);
if (nodes == null) {
result = new TextTreeNode("Empty type system trace");
setRootVisible(true);
} else {
result = new TextTreeNode("Details");
for (MPSTreeNode node : nodes) {
result.add(node);
}
}
return result;
}
private class ShowStateUpdater implements TreeSelectionListener {
private ShowStateUpdater() {
}
@Override
public void valueChanged(TreeSelectionEvent e) {
TreePath path = e.getNewLeadSelectionPath();
if (path == null) {
return;
}
Object treeNode = path.getLastPathComponent();
if (treeNode instanceof TypeSystemTraceTreeNode) {
showState((TypeSystemTraceTreeNode) treeNode);
}
}
}
}
private static AbstractOperation getAssociatedOperation(MPSTreeNode checkedDotOperand) {
if (null != checkedDotOperand) {
return (AbstractOperation) checkedDotOperand.getUserObject();
}
return null;
}
}