// 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.Arrays;
import java.util.Collections;
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 InputBase(newSource, oldSource) {
@Override public FunctionNode getRootFunction() {
return rootFunction;
}
@Override public TextualDiff getTextualDiff() {
return textualDiff;
}
};
}
static LiveEditDiffViewer.Input createCompileErrorViewerInput(
final UpdatableScript.CompileErrorFailure compileError, PushChangesPlan changesPlan,
boolean previewMode) {
final String newSource = changesPlan.getNewSource();
final String oldSource = changesPlan.getScript().getSource();
final int startOffset = compileError.getStartPosition().getOffset();
final int endOffset;
if (compileError.getEndPosition() == null) {
endOffset = startOffset + 1;
} else {
endOffset = compileError.getEndPosition().getOffset();
}
final SourcePosition sourcePosition = new SourcePosition() {
@Override public int getStart() {
return startOffset;
}
@Override public int getEnd() {
return endOffset;
}
};
final TextualDiff fakeTextualDiff = new TextualDiff() {
@Override
public List<Long> getChunks() {
return Arrays.asList(
0L, (long) oldSource.length(), (long) newSource.length());
}
};
final FunctionNode fakeFunctionNode = new FunctionNode() {
@Override public String getName() {
return compileError.getCompilerMessage();
}
@Override public String getStatus() {
return Messages.PushResultParser_COMPILE_ERROR;
}
@Override public List<? extends FunctionNode> children() {
return Collections.emptyList();
}
@Override public SourcePosition getPosition(Side side) {
switch (side) {
case NEW:
return sourcePosition;
default:
return null;
}
}
@Override public FunctionNode getParent() {
return null;
}
};
return new InputBase(newSource, oldSource) {
@Override public FunctionNode getRootFunction() {
return fakeFunctionNode;
}
@Override public TextualDiff getTextualDiff() {
return fakeTextualDiff;
}
};
}
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(), Messages.PushResultParser_SCRIPT);
}
private NodeImpl buildNode(UpdatableScript.OldFunctionNode oldFunction,
String predefinedFunctionName) {
List<NodeImpl> childListFirst = new ArrayList<NodeImpl>();
for (UpdatableScript.OldFunctionNode oldChild : oldFunction.children()) {
NodeImpl nodeImpl = buildNode(oldChild, null);
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,
predefinedFunctionName);
}
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, null);
}
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,
String predefinedFunctionName) {
this.rawFunction = rawFunction;
if (predefinedFunctionName == null) {
this.name = rawFunction.getName();
} else {
this.name = predefinedFunctionName;
}
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;
}
};
}
private static abstract class InputBase implements LiveEditDiffViewer.Input {
private final String newSource;
private final String oldSource;
InputBase(String newSource, String oldSource) {
this.newSource = newSource;
this.oldSource = oldSource;
}
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;
}
};
}
}
}