/******************************************************************************* * Copyright (c) 2010 xored software, Inc. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * xored software, Inc. - initial API and Implementation (Vladislav Kuzkokov) *******************************************************************************/ package org.eclipse.dltk.javascript.core.dom.rewrite; import static org.eclipse.jface.text.TextUtilities.determineLineDelimiter; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import org.eclipse.dltk.compiler.util.Util; import org.eclipse.dltk.javascript.ast.MultiLineComment; import org.eclipse.dltk.javascript.core.dom.BinaryExpression; import org.eclipse.dltk.javascript.core.dom.BinaryOperator; import org.eclipse.dltk.javascript.core.dom.CatchClause; import org.eclipse.dltk.javascript.core.dom.DomPackage; import org.eclipse.dltk.javascript.core.dom.FunctionExpression; import org.eclipse.dltk.javascript.core.dom.Node; import org.eclipse.dltk.javascript.core.dom.Source; import org.eclipse.dltk.javascript.core.dom.Statement; import org.eclipse.dltk.javascript.core.dom.TryStatement; import org.eclipse.dltk.javascript.core.dom.UnaryExpression; import org.eclipse.dltk.javascript.core.dom.UnaryOperator; import org.eclipse.dltk.javascript.core.dom.VariableDeclaration; import org.eclipse.dltk.javascript.core.dom.util.DomSwitch; import org.eclipse.emf.common.util.BasicEList; import org.eclipse.emf.common.util.EList; import org.eclipse.emf.ecore.EAttribute; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EReference; import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.EcorePackage; import org.eclipse.emf.ecore.change.ChangeDescription; import org.eclipse.emf.ecore.change.ChangeKind; import org.eclipse.emf.ecore.change.FeatureChange; import org.eclipse.emf.ecore.change.ListChange; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.text.edits.DeleteEdit; import org.eclipse.text.edits.InsertEdit; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.text.edits.TextEdit; public class RewriteAnalyzer extends DomSwitch<Boolean> { private final ChangeDescription cd; private final Set<Node> generated = new HashSet<Node>(); private final String text; protected final String lineDelimiter; private final TextEdit edit; private MultiLineComment[] comments; public RewriteAnalyzer(ChangeDescription cd, String text) { this(cd, text, determineLineDelimiter(text, Util.LINE_SEPARATOR)); } public RewriteAnalyzer(ChangeDescription cd, String text, String lineDelimiter) { this.cd = cd; this.text = text; this.lineDelimiter = lineDelimiter; this.edit = new MultiTextEdit(); } public void rewrite(Source source) { final CommentContainer container = (CommentContainer) EcoreUtil .getExistingAdapter(source, CommentContainer.class); this.comments = container != null ? container.comments : null; rewrite((Node) source); } void rewrite(Node node) { doSwitch(node); for (EObject obj : node.eContents()) if (!generated.contains(obj)) rewrite((Node) obj); } public TextEdit getEdit() { return edit; } protected void addEdit(TextEdit edit, Node node) { this.edit.addChild(edit); } // Processes EReference only private void processFeature(Node node, FeatureChange fc) { if (fc.getFeature() instanceof EAttribute) { final EAttribute attribute = (EAttribute) fc.getFeature(); if (!attribute.isMany() && attribute.getEAttributeType() == EcorePackage.Literals.ESTRING && EcoreUtil.getAnnotation(attribute, null, "value") != null) { addEdit(new ReplaceEdit(node.getBegin(), node.getEnd() - node.getBegin(), (String) node.eGet(attribute)), node); } return; } if (!fc.getFeature().isMany()) { Node n = (Node) node.eGet(fc.getFeature()); Node o = (Node) fc.getReferenceValue(); int off = o == null ? calcOffset(node, fc.getFeature()) : o .getBegin(); int len = o == null ? 0 : o.getEnd() - o.getBegin(); String value = n == null ? "" : generate(n, node, o == null, off); addEdit(new ReplaceEdit(off, len, value), n); return; } @SuppressWarnings("unchecked") EList<? extends Node> dst = (EList<? extends Node>) node.eGet(fc .getFeature()); EList<Object> src = new BasicEList<Object>(); src.addAll(dst); Set<Node> deleted = new HashSet<Node>(); Set<Node> generated = new HashSet<Node>(); for (ListChange lc : fc.getListChanges()) { if (lc.getKind() != ChangeKind.ADD_LITERAL) generated.add((Node) src.get(lc.getIndex())); if (lc.getKind() == ChangeKind.MOVE_LITERAL) deleted.add((Node) src.get(lc.getIndex())); lc.apply(src); if (lc.getKind() == ChangeKind.ADD_LITERAL) deleted.add((Node) src.get(lc.getIndex())); } if (fc.getListChanges().isEmpty()) { src.clear(); generated.addAll(dst); } List<Node> original = new ArrayList<Node>(src.size()); for (Object obj : src) original.add((Node) obj); Node last = null; for (Node item : original) if (!deleted.contains(item)) last = item; // DELETING ELEMENTS // 1) general case: a,(b,)c,(d,)(e,)f // 2) at the end: a,(b,)c,d(,e)(,f) // 3) all of it: (a)(,b)(,c) // we delete element with following separator so that this will work // with statements and line feeds/semicolons. boolean isLast = last == null; for (int i = 0; i < original.size(); i++) { Node item = original.get(i); if (deleted.contains(item)) { int off = isLast && i != 0 ? original.get(i - 1).getEnd() : getBegin(item); int end = isLast ? item.getEnd() : getBegin(original.get(i + 1)); addEdit(new DeleteEdit(off, end - off), item); } else isLast = item == last; } // OFFSETS // each element is added at the beginning of the next element // or at the end List<Integer> offs = new ArrayList<Integer>(); int cur = original.isEmpty() ? calcOffset(node, fc.getFeature()) : original.get(original.size() - 1).getEnd(); for (int i = dst.size() - 1; i >= 0; i--) { Node item = dst.get(i); if (!generated.contains(item)) { cur = item.getBegin(); } offs.add(cur); } Collections.reverse(offs); // ADDING ELEMENTS // adding is done by the same rules as deleting // cases 1-3 are processed in generateElement isLast = last == null; for (int i = 0; i < dst.size(); i++) { Node item = dst.get(i); if (generated.contains(item)) { int off = offs.get(i); addEdit(new InsertEdit(off, generateElement(item, i == 0, isLast, off)), item); } else isLast = item == last; } } private int getBegin(Node node) { int value = node.getBegin(); if (comments != null) { final MultiLineComment comment = findComment(value); if (comment != null) { for (int i = comment.end(); i < value; ++i) { if (!Character.isWhitespace(text.charAt(i))) { return value; } } return comment.start(); } } return value; } private MultiLineComment findComment(int value) { int low = 0; int high = comments.length; while (low < high) { final int mid = (low + high) >>> 1; final MultiLineComment comment = comments[mid]; final int end = comment.end(); if (end > value) { high = mid; } else if (end < value) { low = mid + 1; } else { return comment; } } return low > 0 ? comments[low - 1] : null; } @Override public Boolean caseNode(Node node) { if (cd.getObjectChanges().get(node) != null) for (FeatureChange fc : cd.getObjectChanges().get(node)) processFeature(node, fc); return true; } @Override public Boolean caseUnaryExpression(UnaryExpression node) { if (cd.getObjectChanges().get(node) != null) for (FeatureChange fc : cd.getObjectChanges().get(node)) if (fc.getFeature() == DomPackage.Literals.UNARY_EXPRESSION__OPERATION) { UnaryOperator n = node.getOperation(); UnaryOperator o = (UnaryOperator) fc.getValue(); int len = o.toString().length(); if (isPostfix(o)) addEdit(new DeleteEdit(node.getEnd() - len, len), node); else addEdit(new DeleteEdit(node.getBegin(), len), node); if (isPostfix(n)) addEdit(new InsertEdit(node.getEnd(), n.toString()), node); else { String r = n.toString(); if (isTextUnary(n)) r += ' '; addEdit(new InsertEdit(node.getBegin(), r), node); } } else processFeature(node, fc); return true; } static boolean isPostfix(Object op) { return op == UnaryOperator.POSTFIX_INC || op == UnaryOperator.POSTFIX_DEC; } static boolean isTextUnary(Object op) { return op == UnaryOperator.DELETE || op == UnaryOperator.VOID || op == UnaryOperator.TYPEOF || op == UnaryOperator.YIELD; } @Override public Boolean caseBinaryExpression(BinaryExpression node) { if (cd.getObjectChanges().get(node) != null) for (FeatureChange fc : cd.getObjectChanges().get(node)) if (fc.getFeature() == DomPackage.Literals.BINARY_EXPRESSION__OPERATION) { String r = node.getOperation().toString(); if (isTextBinary(fc.getValue())) r = ' ' + r + ' '; addEdit(new ReplaceEdit(node.getOperatorPosition(), fc .getValue().toString().length(), r), node); } else processFeature(node, fc); return true; } static boolean isTextBinary(Object op) { return op == BinaryOperator.IN || op == BinaryOperator.INSTANCEOF; } // calculates offset for null references and empty lists private int calcOffset(Node node, EStructuralFeature sf) { EReference ref = (EReference) sf; switch (ref.getEContainingClass().getClassifierID()) { case DomPackage.SOURCE: return node.getBegin(); case DomPackage.BLOCK_STATEMENT: return node.getEnd() - 1; // skip right brace case DomPackage.VARIABLE_STATEMENT: return node.getBegin() + 3; // skip "var" case DomPackage.CONST_STATEMENT: return node.getBegin() + 5; // skip "const" case DomPackage.VARIABLE_DECLARATION: case DomPackage.IF_STATEMENT: case DomPackage.CONTINUE_STATEMENT: case DomPackage.BREAK_STATEMENT: case DomPackage.RETURN_STATEMENT: case DomPackage.SWITCH_ELEMENT: case DomPackage.PARAMETER: return node.getEnd(); case DomPackage.CALL_EXPRESSION: return node.getEnd() - 1; case DomPackage.SWITCH_STATEMENT: throw new IllegalStateException("Empty switch statement"); case DomPackage.CATCH_CLAUSE: CatchClause cc = (CatchClause) node; return cc.getException().getEnd(); case DomPackage.TRY_STATEMENT: TryStatement ts = (TryStatement) node; if (ts.getFinallyClause() != null) return ts.getFinallyClause().getBegin(); return node.getEnd(); case DomPackage.FUNCTION_EXPRESSION: FunctionExpression expr = (FunctionExpression) node; if (ref == DomPackage.Literals.FUNCTION_EXPRESSION__IDENTIFIER) return expr.getParametersPosition() - 1; else return expr.getParametersPosition(); } return -1; } static abstract class NodeSeparator { public abstract void appendTo(Generator gen); } static final NodeSeparator NEWLINE = new NodeSeparator() { @Override public void appendTo(Generator gen) { gen.newLine(); } }; static final NodeSeparator COMMA = new NodeSeparator() { @Override public void appendTo(Generator gen) { gen.append(","); } }; static final NodeSeparator COMMA_NEWLINE = new NodeSeparator() { @Override public void appendTo(Generator gen) { gen.append(","); gen.newLine(); } }; public String generateElement(Node node, boolean first, boolean last, int pos) { Generator gen = new Generator(cd, text, pos, lineDelimiter); final NodeSeparator separator; if (node instanceof Statement) { separator = NEWLINE; } else if (node instanceof VariableDeclaration && ((VariableDeclaration) node).getInitializer() != null) { separator = COMMA_NEWLINE; } else { separator = COMMA; } if (!first && last) separator.appendTo(gen); gen.generate(node); if (!last) separator.appendTo(gen); generated.add(node); return gen.toString(); } public String generate(Node node, Node parent, boolean wasNull, int pos) { Generator gen = new Generator(cd, text, pos, lineDelimiter); if (wasNull && parent.eClass() == DomPackage.Literals.VARIABLE_DECLARATION) gen.append("="); if (wasNull && parent.eClass() == DomPackage.Literals.FUNCTION_EXPRESSION) gen.append(" "); if (wasNull && node.eContainmentFeature() == DomPackage.Literals.CATCH_CLAUSE__FILTER) gen.append(" if "); gen.generate(node); generated.add(node); return gen.toString(); } }