// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.debug.ui.liveedit; import java.util.ArrayList; import java.util.List; import org.chromium.debug.core.model.PushChangesPlan; import org.chromium.debug.core.model.SourceWrapSupport; import org.chromium.debug.ui.liveedit.LiveEditDiffViewer.FunctionNode; import org.chromium.debug.ui.liveedit.LiveEditDiffViewer.Side; import org.chromium.debug.ui.liveedit.LiveEditDiffViewer.SourcePosition; import org.chromium.debug.ui.liveedit.LiveEditDiffViewer.SourceText; import org.chromium.sdk.UpdatableScript; import org.chromium.sdk.UpdatableScript.NewFunctionNode; import org.chromium.sdk.UpdatableScript.OldFunctionNode; import org.chromium.sdk.UpdatableScript.TextualDiff; import org.eclipse.osgi.util.NLS; /** * Parses LiveEdit push result and produces input for {@link LiveEditDiffViewer}. * It is also responsible for providing user-visible explanations about situation with each * function. */ public class PushResultParser { static LiveEditDiffViewer.Input createViewerInput( final UpdatableScript.ChangeDescription changeDescription, PushChangesPlan changesPlan, boolean previewMode) { SourceWrapSupport.Wrapper.Match wrapperMatch = changesPlan.getSourceWrapperMatch(); final String newSource; final String oldSource; final TextualDiff textualDiff; String newSourceRaw = changesPlan.getNewSource(); String oldSourceRaw = changesPlan.getScript().getSource(); int oldPositionOffset; int newPositionOffset; if (wrapperMatch == null) { oldSource = oldSourceRaw; newSource = newSourceRaw; textualDiff = changeDescription.getTextualDiff(); oldPositionOffset = 0; newPositionOffset = 0; } else { String oldSourceComment = NLS.bind( Messages.PushResultParser_WRAPPING_DETECTED, wrapperMatch.getWrapper().getName()); oldSource = oldSourceComment + oldSourceRaw; oldPositionOffset = oldSourceComment.length(); String newSourceComment = Messages.PushResultParser_WRAPPING_AUTOGENERATED; newSource = newSourceComment + wrapperMatch.wrap(newSourceRaw); newPositionOffset = newSourceComment.length(); textualDiff = shiftNewPositions(changeDescription.getTextualDiff(), oldSourceComment.length(), newSourceComment.length(), wrapperMatch.getPrefixLength(), wrapperMatch.getSuffixLength(), oldSourceRaw.length(), newSourceRaw.length()); } TreeBuilder builder = new TreeBuilder(previewMode, oldPositionOffset, newPositionOffset); final FunctionNode rootFunction = builder.build(changeDescription); return new LiveEditDiffViewer.Input() { public SourceText getNewSource() { return new SourceText() { public String getText() { return newSource; } public String getTitle() { return Messages.PushResultParser_LOCAL_FILE; } }; } public SourceText getOldSource() { return new SourceText() { public String getText() { return oldSource; } public String getTitle() { return Messages.PushResultParser_SCRIPT_IN_VM; } }; } public FunctionNode getRootFunction() { return rootFunction; } @Override public TextualDiff getTextualDiff() { return textualDiff; } }; } private static class TreeBuilder { private final StatusRenderer statusRenderer; private final boolean hideOldVersion; private final int oldPositionOffset; private final int newPositionOffset; public TreeBuilder(boolean previewOnly, int oldPositionOffset, int newPositionOffset) { if (previewOnly) { this.statusRenderer = PREVIEW_STATUS_RENDERER; } else { this.statusRenderer = RESULT_STATUS_RENDERER; } this.hideOldVersion = !previewOnly; this.oldPositionOffset = oldPositionOffset; this.newPositionOffset = newPositionOffset; } public FunctionNode build(UpdatableScript.ChangeDescription changeDescription) { return buildNode(changeDescription.getChangeTree()); } private NodeImpl buildNode(UpdatableScript.OldFunctionNode oldFunction) { List<NodeImpl> childListFirst = new ArrayList<NodeImpl>(); for (UpdatableScript.OldFunctionNode oldChild : oldFunction.children()) { NodeImpl nodeImpl = buildNode(oldChild); childListFirst.add(nodeImpl); } List<NodeImpl> childListSecond = new ArrayList<NodeImpl>(); for (NewFunctionNode newChild : oldFunction.newChildren()) { NodeImpl nodeImpl = buildNode(newChild, newPositionOffset); childListSecond.add(nodeImpl); } // Merge lists by positions SIDE.NEW List<NodeImpl> childList = new ArrayList<NodeImpl>(); { int pos1 = 0; int pos2 = 0; while (true) { if (pos1 == childListFirst.size()) { childList.addAll(childListSecond.subList(pos2, childListSecond.size())); break; } if (pos2 == childListSecond.size()) { childList.addAll(childListFirst.subList(pos1, childListFirst.size())); break; } SourcePosition firstChildSourcePos = childListFirst.get(pos1).getPosition(Side.NEW); if (firstChildSourcePos == null) { childList.add(childListFirst.get(pos1)); pos1++; } else { if (firstChildSourcePos.getStart() < childListSecond.get(pos2).getPosition(Side.NEW).getStart()) { childList.add(childListFirst.get(pos1)); pos1++; } else { childList.add(childListSecond.get(pos2)); pos2++; } } } } return new NodeImpl(oldFunction, createPosition(oldFunction.getPositions(), oldPositionOffset), createPosition(oldFunction.getNewPositions(), newPositionOffset), childList); } private NodeImpl buildNode(UpdatableScript.NewFunctionNode newFunction, int newPositionOffset) { List<NodeImpl> childList = new ArrayList<NodeImpl>(); for (UpdatableScript.NewFunctionNode newChild : newFunction.children()) { NodeImpl nodeImpl = buildNode(newChild, newPositionOffset); childList.add(nodeImpl); } return new NodeImpl(newFunction, null, createPosition(newFunction.getPositions(), newPositionOffset), childList); } private static SourcePosition createPosition( final UpdatableScript.FunctionPositions positions, final int offset) { if (positions == null) { return null; } return new SourcePosition() { public int getStart() { return (int) positions.getStart() + offset; } public int getEnd() { return (int) positions.getEnd() + offset; } }; } private class NodeImpl implements FunctionNode { private final UpdatableScript.FunctionNode<?> rawFunction; private final String name; private final SourcePosition oldPosition; private final SourcePosition newPosition; private final List<? extends FunctionNode> childList; private FunctionNode parent = null; private NodeImpl(UpdatableScript.FunctionNode<?> rawFunction, SourcePosition oldPosition, SourcePosition newPosition, List<? extends NodeImpl> childList) { this.rawFunction = rawFunction; this.name = rawFunction.getName(); this.oldPosition = oldPosition; this.newPosition = newPosition; this.childList = childList; for (NodeImpl child : childList) { child.parent = this; } } public List<? extends FunctionNode> children() { return childList; } public String getName() { return name; } public String getStatus() { return statusRenderer.getStatus(rawFunction, this); } public FunctionNode getParent() { return parent; } public SourcePosition getPosition(Side side) { switch (side) { case OLD: { if (newPosition != null && hideOldVersion && rawFunction.asOldFunction() != null && rawFunction.asOldFunction().getStatus() != UpdatableScript.ChangeStatus.DAMAGED) { return null; } return oldPosition; } case NEW: return newPosition; default: throw new RuntimeException(); } } protected SourcePosition getOldPositionInternal() { return oldPosition; } protected SourcePosition getNewPositionInternal() { return newPosition; } } private static abstract class StatusRenderer { abstract String getStatus(UpdatableScript.FunctionNode<?> rawFunction, NodeImpl nodeImpl); } private static final StatusRenderer PREVIEW_STATUS_RENDERER = new StatusRenderer() { @Override String getStatus(UpdatableScript.FunctionNode<?> rawFunction, NodeImpl nodeImpl) { OldFunctionNode asOldFunction = rawFunction.asOldFunction(); if (asOldFunction == null) { return Messages.PushResultParser_NEW_FUNCTION; } else { switch (rawFunction.asOldFunction().getStatus()) { case UNCHANGED: return ""; //$NON-NLS-1$ case NESTED_CHANGED: return Messages.PushResultParser_PREVIEW_CHANGED; case CODE_PATCHED: return Messages.PushResultParser_PREVIEW_PATCHED; case DAMAGED: { String message; if (nodeImpl.getPosition(Side.NEW) == null) { message = Messages.PushResultParser_PREVIEW_DAMAGED; } else { message = Messages.PushResultParser_PREVIEW_DAMAGED_2; } String explanation = rawFunction.asOldFunction().getStatusExplanation(); if (explanation != null) { message = message + "\n[" + explanation + "]"; //$NON-NLS-1$ //$NON-NLS-2$ } return message; } default: return Messages.PushResultParser_PREVIEW_UNKNOWN; } } } }; private static final StatusRenderer RESULT_STATUS_RENDERER = new StatusRenderer() { @Override String getStatus(UpdatableScript.FunctionNode<?> rawFunction, NodeImpl nodeImpl) { OldFunctionNode asOldFunction = rawFunction.asOldFunction(); if (asOldFunction == null) { return Messages.PushResultParser_RESULT_NEW_FUNCTION; } else { switch (asOldFunction.getStatus()) { case UNCHANGED: return ""; //$NON-NLS-1$ case NESTED_CHANGED: return Messages.PushResultParser_RESULT_CHANGED; case CODE_PATCHED: return Messages.PushResultParser_RESULT_PATHCED; case DAMAGED: { String message; if (nodeImpl.getNewPositionInternal() == null) { message = Messages.PushResultParser_RESULT_DAMAGED; } else { message = Messages.PushResultParser_RESULT_DAMAGED_2; } String explanation = asOldFunction.getStatusExplanation(); if (explanation != null) { message = message + "\n[" + explanation + "]"; //$NON-NLS-1$ //$NON-NLS-2$ } return message; } default: return Messages.PushResultParser_RESULT_UNKNOWN; } } } }; } // There is a problem with this approach. We have sent a source with prefix and suffix // and expect V8 to recognize them as such. However V8 diff algorithm theoretically // can treat part of the suffix/prefix as original source, while matching unchanged text // nearby as diff. private static TextualDiff shiftNewPositions(TextualDiff textualDiff, int oldCommentLength, int newCommentLength, int prefixLength, int suffixLength, int originalOldLength, int originalNewLength) { List<Long> originalChunks = textualDiff.getChunks(); final List<Long> shiftedChunks = new ArrayList<Long>(originalChunks.size() + 3 + 3); shiftedChunks.add((long) 0); shiftedChunks.add((long) (oldCommentLength + prefixLength)); shiftedChunks.add((long) (newCommentLength + prefixLength)); for (int i = 0; i < originalChunks.size(); i += 3) { shiftedChunks.add(originalChunks.get(i + 0) + oldCommentLength); shiftedChunks.add(originalChunks.get(i + 1) + oldCommentLength); shiftedChunks.add(originalChunks.get(i + 2) + newCommentLength); } shiftedChunks.add((long) originalOldLength + oldCommentLength - suffixLength); shiftedChunks.add((long) originalOldLength + oldCommentLength); shiftedChunks.add((long) originalNewLength + newCommentLength + prefixLength + suffixLength); return new TextualDiff() { @Override public List<Long> getChunks() { return shiftedChunks; } }; } }