/** * Copyright (c) 2006 IBM Corporation and others. * 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: * IBM - Initial API and implementation */ package org.eclipse.emf.codegen.merge.java.facade.ast; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.EnumConstantDeclaration; import org.eclipse.jdt.core.dom.EnumDeclaration; import org.eclipse.jdt.core.dom.PackageDeclaration; import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; import org.eclipse.jdt.core.dom.rewrite.ITrackedNodePosition; import org.eclipse.jdt.core.dom.rewrite.ListRewrite; import org.eclipse.jdt.core.formatter.IndentManipulation; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.text.edits.InsertEdit; import org.eclipse.text.edits.MalformedTreeException; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.text.edits.TextEdit; import org.eclipse.emf.codegen.merge.java.facade.FacadeHelper; import org.eclipse.emf.codegen.merge.java.facade.JCompilationUnit; import org.eclipse.emf.codegen.merge.java.facade.JNode; import org.eclipse.emf.codegen.merge.java.facade.JType; /** * Wraps {@link CompilationUnit} object. * * @since 2.2.0 */ public class ASTJCompilationUnit extends ASTJNode<CompilationUnit> implements JCompilationUnit { /** * Name for the name property of the compilation unit. */ public static final String NAME_PROPERTY = "ASTJCompilationUnit.name"; /** * Pattern to extract and replace header. * Header is any number of block or line comments, spaces or newline characters * before any Java code (package, import or type declaration). */ protected final static Pattern HEADER_PATTERN = Pattern.compile("^(?:(?:/\\*(?:.|[\\n\\r])*?\\*/)|(?://.*(?:[\\n\\r])+)|(?:\\s+))+"); /** * Map of nodes (<code>ASTNode</code>) to contents of the node (<code>String</code>). * This map is used during rewrite to track the nodes positions and set the exact contents of them. */ private Map<ASTNode, String> allTrackedContentsMap = new HashMap<ASTNode, String>(); /** * Set of all nodes that have been commented out */ private Set<ASTNode> commentedOutNodes = null; /** * Header of the compilation unit to be set during rewrite. * If it is null, the header will not be modified. */ protected String headerString = null; /** * Original contents of the compilation unit */ protected char[] originalContents; /** * @param compilationUnit */ public ASTJCompilationUnit(CompilationUnit compilationUnit) { super(compilationUnit); } @Override public void dispose() { allTrackedContentsMap.clear(); headerString = null; originalContents = null; if (commentedOutNodes != null) { commentedOutNodes.clear(); commentedOutNodes = null; } super.dispose(); } /** * Sets original contents of the compilation unit to be used for converting nodes * to strings. * * @param originalContents */ public void setOriginalContents(char[] originalContents) { this.originalContents = originalContents; } /** * @return original contents of the compilation unit */ public char[] getOriginalContents() { return originalContents; } /** * Returns the name of the main type suffixed with java extension. * <p> * In the absence of type defaults to original name that compilation unit was created with. * <p> * Created for compatibility with JDOM implementation of <code>getName()</code> for {@link JCompilationUnit}. * * @return name of the main type suffixed with java extension * * @see org.eclipse.emf.codegen.merge.java.facade.JNode#getName() */ public String getName() { JType type = getFacadeHelper().getMainType(this); return type != null ? type.getName() + ".java" : (String)getWrappedObject().getProperty(NAME_PROPERTY); } /** * Sets the name of the main type of compilation unit. * * @see FacadeHelper#getMainType(JCompilationUnit) * @see org.eclipse.emf.codegen.merge.java.facade.JNode#setName(java.lang.String) * @see org.eclipse.emf.codegen.merge.java.facade.JNode#getQualifiedName() */ public void setName(String name) { JType type = getFacadeHelper().getMainType(this); if (type != null) { type.setName(name); } else { getASTNode().setProperty(NAME_PROPERTY, name); } } /** * Same as the <code>getName()</code>. * * @see org.eclipse.emf.codegen.merge.java.facade.AbstractJNode#computeQualifiedName() */ @Override protected String computeQualifiedName() { return getName(); } /** * Returns the list of children in order: package declaration (<code>JPackage</code>), imports (<code>JImport</code>), and types (<code>JType</code>). * * @see org.eclipse.emf.codegen.merge.java.facade.AbstractJNode#getChildren() */ @Override public List<JNode> getChildren() { if (!isDisposed()) { CompilationUnit astCompilationUnit = getASTNode(); List<JNode> children = new ArrayList<JNode>(); PackageDeclaration astPackage = astCompilationUnit.getPackage(); if (astPackage != null) { JNode child = getFacadeHelper().convertToNode(astPackage); if (child != null) { children.add(child); } } ListRewrite importsListRewrite = rewriter.getListRewrite(astCompilationUnit, CompilationUnit.IMPORTS_PROPERTY); for (Object importDeclaration : importsListRewrite.getRewrittenList()) { JNode child = getFacadeHelper().convertToNode(importDeclaration); if (child != null) { children.add(child); } } ListRewrite typesListRewrite = rewriter.getListRewrite(astCompilationUnit, CompilationUnit.TYPES_PROPERTY); for (Object type : typesListRewrite.getRewrittenList()) { JNode child = getFacadeHelper().convertToNode(type); if (child != null) { children.add(child); } } if (!children.isEmpty()) { return Collections.unmodifiableList(children); } } return Collections.emptyList(); } public String getHeader() { if (headerString == null) { Matcher matcher = HEADER_PATTERN.matcher(new String(originalContents)); if (matcher.find()) { String headerString = matcher.group(); if (ASTFacadeHelper.DEBUG) { getFacadeHelper().logInfo("Got header <" + headerString + ">"); } return headerString; } else { return ""; } } return headerString; } /** * This implementation remembers the header string, and does replacement of the header in the final * document after any other changes. * <p> * For the files with no package declaration, * the new header might be inserted at the beginning of the file instead of being replaced. * * @see org.eclipse.emf.codegen.merge.java.facade.JCompilationUnit#setHeader(java.lang.String) */ public void setHeader(String header) { if (header != null) { this.headerString = header; } } /** * Method to replace the header in the given document. * * @param targetDoc */ protected void setHeader(IDocument targetDoc) { String targetDocString = targetDoc.get(); Matcher matcher = HEADER_PATTERN.matcher(targetDocString); // replace or append if (matcher.find()) { targetDocString = headerString.concat(targetDocString.substring(matcher.end())); } else { targetDocString = headerString.concat(targetDocString); } targetDoc.set(targetDocString); } /** * Returns the rewritten text after applying all the changes made to JNode tree. * * @see org.eclipse.emf.codegen.merge.java.facade.ast.ASTJNode#getContents() */ @Override public String getContents() { // enable tracking for nodes that have string content AbstractRewriter contentsReplacer = null; if (allTrackedContentsMap != null && !allTrackedContentsMap.isEmpty()) { contentsReplacer = new NodeContentsReplacer(); } // enable tracking for commented out nodes AbstractRewriter commenter = null; if (commentedOutNodes != null && !commentedOutNodes.isEmpty()) { commenter = new NodeCommenter(); } String contents = new String(originalContents); IDocument targetDoc = new Document(contents); TextEdit edits = rewriter.rewriteAST(targetDoc, getFacadeHelper().getJavaCoreOptions()); if (edits.getChildrenSize() != 0 || edits.getLength() != 0 || commenter != null || contentsReplacer != null || headerString != null) { // apply changes using ASTRewrite // try { edits.apply(targetDoc); if (ASTFacadeHelper.DEBUG) { getFacadeHelper().logInfo("Document after ASTRewrite:\n<" + targetDoc.get() + ">\nEnd of document."); } } catch (MalformedTreeException e) { if (ASTFacadeHelper.DEBUG) { getFacadeHelper().logError("Error applying edits: ", e); } } catch (BadLocationException e) { if (ASTFacadeHelper.DEBUG) { getFacadeHelper().logError("Error applying edits: ", e); } } // apply additional edits like replacing or commenting out nodes // try { TextEdit additionalEdits = null; if (contentsReplacer != null) { additionalEdits = contentsReplacer.createEdits(additionalEdits, targetDoc); } if (commenter != null) { additionalEdits = commenter.createEdits(additionalEdits, targetDoc); } if (additionalEdits != null) { additionalEdits.apply(targetDoc); } } catch (Exception e) { if (ASTFacadeHelper.DEBUG) { getFacadeHelper().logError("Error creating and applying replace edits: ", e); } } // apply header if (headerString != null) { setHeader(targetDoc); } contents = targetDoc.get(); } return contents; } /* (non-Javadoc) * @see org.eclipse.emf.codegen.merge.java.facade.ast.ASTJNode#addChild(org.eclipse.emf.codegen.merge.java.facade.JNode) */ @Override public boolean addChild(ASTJNode<?> child) { if (child.getParent() != null) { return false; } if (child instanceof ASTJImport) { insertLast(child, CompilationUnit.IMPORTS_PROPERTY); } else if (child instanceof ASTJAbstractType<?>) { insertLast(child, CompilationUnit.TYPES_PROPERTY); } else if (child instanceof ASTJPackage) { setNodeProperty(getASTNode(), child.getASTNode(), CompilationUnit.PACKAGE_PROPERTY); } else { return false; } child.setParent(this); return true; } /* (non-Javadoc) * @see org.eclipse.emf.codegen.merge.java.facade.ast.ASTJNode#insertSibling(org.eclipse.emf.codegen.merge.java.facade.JNode, org.eclipse.emf.codegen.merge.java.facade.JNode, boolean) */ @Override public boolean insertSibling(ASTJNode<?> node, ASTJNode<?> newSibling, boolean before) { if (newSibling.getParent() != null) { return false; } if (newSibling instanceof ASTJImport) { if (node instanceof ASTJImport) { insert(newSibling, CompilationUnit.IMPORTS_PROPERTY, node, before); } else if (node instanceof ASTJPackage) { insertFirst(newSibling, CompilationUnit.IMPORTS_PROPERTY); } else if (node instanceof ASTJAbstractType<?>) { insertLast(newSibling, CompilationUnit.IMPORTS_PROPERTY); } else { return false; } } else if (newSibling instanceof ASTJAbstractType<?>) { if (node instanceof ASTJAbstractType<?>) { insert(newSibling, CompilationUnit.TYPES_PROPERTY, node, before); } else if (node instanceof ASTJImport) { insertFirst(newSibling, CompilationUnit.TYPES_PROPERTY); } else if (node instanceof ASTJPackage) { insertFirst(newSibling, CompilationUnit.TYPES_PROPERTY); } else { return false; } } else if (newSibling instanceof ASTJPackage) { setNodeProperty(getASTNode(), newSibling.getASTNode(), CompilationUnit.PACKAGE_PROPERTY); } else { return false; } newSibling.setParent(this); return true; } /** * Removes the given node. * <p> * This implementation will not perform moving of the node if the node is inserted after being removed. * Hence, the removed node must not be inserted again. * * @see org.eclipse.emf.codegen.merge.java.facade.ast.ASTJNode#remove(ASTJNode) */ @Override public boolean remove(ASTJNode<?> node) { if (node.getParent() != this) { return false; } if (node instanceof ASTJImport) { remove(node, CompilationUnit.IMPORTS_PROPERTY); } else if (node instanceof ASTJAbstractType<?>) { remove(node, CompilationUnit.TYPES_PROPERTY); } else if (node instanceof ASTJPackage) { setNodeProperty(getASTNode(), null, CompilationUnit.PACKAGE_PROPERTY); } else { return false; } node.setParent(null); return true; } /** * @return map of all nodes of the compilation unit and its children (<code>ASTNode</code> to <code>String</code> contents of the node). * <p> * This map will be used during rewrite to track the nodes and set the exact contents of them. */ protected Map<ASTNode, String> getAllTrackedContentsMap() { return allTrackedContentsMap; } /** * @return set of nodes that have been commented out */ protected Set<ASTNode> getCommentedOutNodes() { if (commentedOutNodes == null) { commentedOutNodes = new HashSet<ASTNode>(); } return commentedOutNodes; } /** * Base class for additional rewriters used during rewrite process. */ protected abstract class AbstractRewriter { /** * Creates additional edits to be applied on the document * * @param existingEdits existing edits on * @param doc document after call to {@link ASTRewrite#rewriteAST()} or {@link ASTRewrite#rewriteAST(IDocument, Map)}. * @return existingEdits with new edits added */ public TextEdit createEdits(TextEdit existingEdits, IDocument doc) { if (existingEdits == null) { existingEdits = new MultiTextEdit(); } return addEdits(existingEdits, doc); } protected abstract TextEdit addEdits(TextEdit existingEdits, IDocument doc); } protected class NodeContentsReplacer extends AbstractRewriter { protected Map<ITrackedNodePosition, String> trackedNodePositionsMap; /** * Enables tracking for all nodes in tracked contents map ({@link #getAllTrackedContentsMap()}). * <p> * This constructor must be called before call to {@link ASTRewrite#rewriteAST()} or {@link ASTRewrite#rewriteAST(IDocument, Map)}. * * @see ASTRewrite#track(ASTNode) */ public NodeContentsReplacer() { Map<ASTNode, String> allTrackedContentsMap = getAllTrackedContentsMap(); trackedNodePositionsMap = new HashMap<ITrackedNodePosition, String>(Math.max((int)(allTrackedContentsMap.keySet().size() / .75f) + 1, 16)); for (ASTNode node : allTrackedContentsMap.keySet()) { try { ITrackedNodePosition trackedNodePosition = rewriter.track(node); trackedNodePositionsMap.put(trackedNodePosition, allTrackedContentsMap.get(node)); } catch (Exception e) { if (ASTFacadeHelper.DEBUG) { getFacadeHelper().logError("Enabling tracking in " + getName(), e); } } } } /** * Creates a {@link TextEdit} that replaces contents of all tracked nodes. * * @param existingEdits existing edits on * @param doc document after call to {@link ASTRewrite#rewriteAST()} or {@link ASTRewrite#rewriteAST(IDocument, Map)}. * @return existingEdits with new edits added * * @see #trackedNodePositionsMap */ @Override protected TextEdit addEdits(TextEdit existingEdits, IDocument doc) { for (Map.Entry<ITrackedNodePosition, String> entry : trackedNodePositionsMap.entrySet()) { ITrackedNodePosition position = entry.getKey(); String contents = entry.getValue(); try { ReplaceEdit replaceEdit = new ReplaceEdit(position.getStartPosition(), position.getLength(), contents); existingEdits.addChild(replaceEdit); } // this should never happen catch (Exception e) { // log the error, ignore the change and continue if (ASTFacadeHelper.DEBUG) { getFacadeHelper().logError("Creating ReplaceEdit for <" + contents + "> in " + getName(), e); } } } return existingEdits; } } protected class NodeCommenter extends AbstractRewriter { /** * String to be inserted at the beginning of lines to indicate the line comment */ protected static final String LINE_COMMENT_STRING = "//"; protected static final String EMPTY_STRING = ""; /** * Map of commented out nodes to their tracked positions */ protected Map<ASTNode, ITrackedNodePosition> commentedOutPositions = new HashMap<ASTNode, ITrackedNodePosition>(); /** * Responsible for inserting line breaks at the beginning and the end of the commented out nodes */ protected LineBreakInserter lineBreakInserter; /** * Map of insert offsets to InsertEdit objects created. * Used to prevent adding different InsertEdit objects at the same offset. */ protected Map<Integer, InsertEdit> addedInsertEdits = new HashMap<Integer, InsertEdit>(); /** * List of currently added text edits that have to be reverted (removed) in case of an exception. */ protected List<TextEdit> textEditsToRevert = new ArrayList<TextEdit>(); /** * Document after call to {@link ASTRewrite#rewriteAST()} or {@link ASTRewrite#rewriteAST(IDocument, Map)}. */ protected IDocument doc; /** * Enables tracking for all commented out nodes. * <p> * This constructor must be called before call to {@link ASTRewrite#rewriteAST()} or {@link ASTRewrite#rewriteAST(IDocument, Map)}. * * @see ASTRewrite#track(ASTNode) */ public NodeCommenter() { for (ASTNode node : getCommentedOutNodes()) { try { commentedOutPositions.put(node, rewriter.track(node)); } catch (Exception e) { if (ASTFacadeHelper.DEBUG) { getFacadeHelper().logError("Enabling tracking in " + getName(), e); } } } } /** * Adds a {@link TextEdit} that comments out required nodes. * * @param existingEdits existing edits on * @param doc document after call to {@link ASTRewrite#rewriteAST()} or {@link ASTRewrite#rewriteAST(IDocument, Map)}. * @return existingEdits with new edits added * * @see NodeContentsReplacer#trackedNodePositionsMap */ @Override protected TextEdit addEdits(TextEdit existingEdits, IDocument doc) { this.doc = doc; lineBreakInserter = new LineBreakInserter(); // loop for all commented out nodes for (Map.Entry<ASTNode, ITrackedNodePosition> entry : commentedOutPositions.entrySet()) { ASTNode node = entry.getKey(); ITrackedNodePosition nodePosition = entry.getValue(); try { // insert line break and comment out the first line if needed // note that first line might move backward beyond node start (e.g. if a comma of the previous enum constant has been commented out) int firstLine = addLineBreakBeforeNode(existingEdits, nodePosition, node); // comment out all lines of the node itself, from firstLine to last line int lastLine = doc.getLineOfOffset(nodePosition.getStartPosition() + nodePosition.getLength()); commentOutLines(existingEdits, firstLine, lastLine); // if the node is less than 1 line long, but its contents is replaced by multiple lines, // comment out each line in it if (firstLine == lastLine && getAllTrackedContentsMap().containsKey(node)) { findAndCommentOutReplaceEdit(existingEdits, nodePosition); } // if there is anything after the node on the same line, insert line break addLineBreakAfterNode(existingEdits, nodePosition, node); // reset text edits; since this node is processed successfully, there is no need to revert these changes textEditsToRevert.clear(); } catch (Exception e) { // revert all current edits for this node for (TextEdit edit : textEditsToRevert) { existingEdits.removeChild(edit); } textEditsToRevert.clear(); // log the error, ignore the change and continue if (ASTFacadeHelper.DEBUG) { try { getFacadeHelper().logError( "Unable to comment out <" + doc.get().substring(nodePosition.getStartPosition(), nodePosition.getLength()) + "> in " + getName() + " : " + e.toString() + ". There should be no tracked changes to commented out nodes."); } catch (Exception innerException) { getFacadeHelper().logError( "Unable to comment out node in " + getName() + " : " + e.toString() + ". Unable to get contents of the node either : " + innerException.toString() + ". There should be no tracked changes to commented out nodes."); } } } } return existingEdits; } /** * Add line break before the node if needed. If there was a line break added at this position (e.g. after the previous node), replace it. * Added line break will contain the marker to comment out the first line of the node. * If <code>InsertEdit</code> has been created, the returned line number is the line after the line break, i.e. the next * line that has to be commented out. * If no changes are made, returned line number is the first line of the node. * * @param existingEdits * @param nodePosition * @param node * @return line number of the next line that has to be commented out * @throws BadLocationException * * @see LineBreakInserter#createLineBreakBeforeNode(ITrackedNodePosition, ASTNode) */ protected int addLineBreakBeforeNode(TextEdit existingEdits, ITrackedNodePosition nodePosition, ASTNode node) throws BadLocationException { // insert line break at the first line if there is something before the node start and the beginning of the line InsertEdit lineBreakEdit = lineBreakInserter.createLineBreakBeforeNode(nodePosition, node); if (lineBreakEdit != null) { // replace existing InsertEdit at this position // (case when previous node ends at the same position that current node starts at) InsertEdit existingEdit = addedInsertEdits.get(lineBreakEdit.getOffset()); if (existingEdit != null) { existingEdits.removeChild(existingEdit); addedInsertEdits.remove(existingEdit); } existingEdits.addChild(lineBreakEdit); textEditsToRevert.add(lineBreakEdit); addedInsertEdits.put(lineBreakEdit.getOffset(), lineBreakEdit); // this line has been commented out, return next line number return doc.getLineOfOffset(lineBreakEdit.getOffset()) + 1; } else { // return the line number of the start of the node return doc.getLineOfOffset(nodePosition.getStartPosition()); } } /** * Creates and adds <code>InsertEdit</code>s that comment out all lines between <code>firstLine</code> and * <code>lastLine</code> inclusively. * <p> * If there is a ReplaceEdit that covers positions where <code>InsertEdit</code>s are inserted, * then ReplaceEdit is replaced by another ReplaceEdit with modified text with all lines commented out. * <p> * If there is any other problem adding new <code>InsertEdit</code>s to existing edits, original exception * is re-thrown. * * @param existingEdits * @param firstLine * @param lastLine * @throws BadLocationException */ protected void commentOutLines(TextEdit existingEdits, int firstLine, int lastLine) throws BadLocationException { for (int i = firstLine; i <= lastLine; i++) { InsertEdit edit = new InsertEdit(doc.getLineOffset(i), LINE_COMMENT_STRING); try { existingEdits.addChild(edit); textEditsToRevert.add(edit); addedInsertEdits.put(edit.getOffset(), edit); } catch (MalformedTreeException e) { // handle the case when there is a replace edit that covers these lines TextEdit causeEdit = e.getChild(); if (causeEdit instanceof ReplaceEdit) { ReplaceEdit newReplaceEdit = commentOutReplaceEdit((ReplaceEdit)causeEdit); // skip all lines that replace edit covers i = doc.getLineOfOffset(newReplaceEdit.getOffset() + newReplaceEdit.getLength()); } else { // should not happen, re-throw exception throw e; } } } } /** * Replaces given {@link ReplaceEdit} by new ReplaceEdit with each line commented out. * <p> * New ReplaceEdit has the same offset and length as the given ReplaceEdit. Text of new ReplaceEdit * has each line but the first one commented out. Given ReplaceEdit is removed from its parent, * and new ReplaceEdit is inserted in its place. * @param replaceEdit * @return new ReplaceEdit */ protected ReplaceEdit commentOutReplaceEdit(ReplaceEdit replaceEdit) { TextEdit parent = replaceEdit.getParent(); String newText = commentOutEachLine(replaceEdit.getText()); ReplaceEdit newEdit = new ReplaceEdit(replaceEdit.getOffset(), replaceEdit.getLength(), newText); parent.removeChild(replaceEdit); parent.addChild(newEdit); return newEdit; } /** * Finds first ReplaceEdit in existing edits that covers node position range, and comments out * each line in it. * <p> * This method is used in the case when existing replace edit covers only a part of one line, * but the contents that it replaces is longer than 1 line. In this case, such replace edit * will be found and its contents changed by this method. * * @param existingEdits * @param nodePosition range of existing node that has a corresponding ReplaceEdit for node's range * @see #commentOutReplaceEdit(ReplaceEdit) */ protected void findAndCommentOutReplaceEdit(TextEdit existingEdits, ITrackedNodePosition nodePosition) { // create and try to add dummy edit to find the ReplaceEdit // this should be faster than lookup since underneath of addChild() binary search is used ReplaceEdit dummyEdit = new ReplaceEdit(nodePosition.getStartPosition() + 1, 0, ""); try { existingEdits.addChild(dummyEdit); } catch (MalformedTreeException e) { TextEdit causeEdit = e.getChild(); if (causeEdit instanceof ReplaceEdit) { commentOutReplaceEdit((ReplaceEdit)causeEdit); } else if (ASTFacadeHelper.DEBUG) { // this should never happen getFacadeHelper().logError("Unable to find ReplaceEdit for node in " + getName() + " : " + e.toString()); } } finally { // make sure that dummy edit is not in the tree try { existingEdits.removeChild(dummyEdit); } catch (Exception e) { // Ignore } } } /** * Comments out each line but the first one in the given text, and returns resulting new text. * @param text * @return new text with each line but the first one commented out */ protected String commentOutEachLine(String text) { // assume length will grow by 10% (average line length is 20 characters) StringBuilder sb = new StringBuilder(text.length() + text.length() / 10); char[] textContent = text.toCharArray(); int lastPos = 0, currentPos = 0; for (int i = 0; i < textContent.length; i++) { if (textContent[i] == '\n') { currentPos = i; } else if (textContent[i] == '\r') { if (i + 1 < textContent.length && textContent[i + 1] == '\n') { // next position is checked as well currentPos = ++i; } else { currentPos = i; } } if (lastPos != currentPos) { // char at currentPos is copied as well sb.append(textContent, lastPos, currentPos - lastPos + 1); sb.append(LINE_COMMENT_STRING); // lastPos, currentPos points at the next chars that will be copied later lastPos = ++currentPos; } } // copy last piece if any if (currentPos < textContent.length) { sb.append(textContent, currentPos, textContent.length - currentPos); } return sb.toString(); } /** * If there is anything after the node, inserts the line break to prevent commenting * out extra content. * * @param existingEdits * @param nodePosition * @param node * @throws BadLocationException * * @see LineBreakInserter#createLineBreakAfterNode(ITrackedNodePosition, ASTNode) */ protected void addLineBreakAfterNode(TextEdit existingEdits, ITrackedNodePosition nodePosition, ASTNode node) throws BadLocationException { InsertEdit lineBreakEdit = lineBreakInserter.createLineBreakAfterNode(nodePosition, node); if (lineBreakEdit != null) { // do not add a new line if there is one InsertEdit existingEdit = addedInsertEdits.get(lineBreakEdit.getOffset()); if (existingEdit == null) { existingEdits.addChild(lineBreakEdit); textEditsToRevert.add(lineBreakEdit); addedInsertEdits.put(lineBreakEdit.getOffset(), lineBreakEdit); } } } /** * Class that inserts extra line breaks between nodes when nodes are being commented out. * * @see #createLineBreakBeforeNode(ITrackedNodePosition, ASTNode) * @see #createLineBreakAfterNode(ITrackedNodePosition, ASTNode) */ protected class LineBreakInserter { protected char[] charContent; protected LineBreakInserter() { this.charContent = getDocument().get().toCharArray(); } /** * @return document to be used to lookup line numbers and line information */ protected IDocument getDocument() { return doc; } /** * Creates line break at the beginning of the node when there is another node declared at the same line. * The returned edit also contains "//" after the line break - the first line of the node becomes already commented out. * @param nodePosition * @param node * * @return <code>InsertEdit</code> or <code>null</code> if none required * @throws BadLocationException */ protected InsertEdit createLineBreakBeforeNode(ITrackedNodePosition nodePosition, ASTNode node) throws BadLocationException { int startPos = nodePosition.getStartPosition(); IRegion lineInfo = getDocument().getLineInformationOfOffset(startPos); // if needed, comment out a comma of the previous enum constant if (node.getNodeType() == ASTNode.ENUM_CONSTANT_DECLARATION) { InsertEdit insertEdit = commentOutEnumConstantSeparator((EnumConstantDeclaration)node, lineInfo, nodePosition); if (insertEdit != null) { return insertEdit; } } // if there is anything before the node on the same line, create line break and comment out the first line of the node if (!isWhitespace(lineInfo.getOffset(), startPos)) { return new InsertEdit(startPos, createLineBreakString(lineInfo.getOffset(), true)); } else { // there is only whitespace on this line - no need for line break return null; } } /** * Creates line break at the end of the node when there is another node declared at the same line. * @param nodePosition * @param node * * @return <code>InsertEdit</code> or <code>null</code> if none required * @throws BadLocationException */ protected InsertEdit createLineBreakAfterNode(ITrackedNodePosition nodePosition, ASTNode node) throws BadLocationException { int endPos = nodePosition.getStartPosition() + nodePosition.getLength(); IRegion lineInfo = getDocument().getLineInformationOfOffset(endPos); // for enum constants, insert line break after the comma if there is anything after the comma if (node.getNodeType() == ASTNode.ENUM_CONSTANT_DECLARATION) { return createLineBreakAfterEnumConstant(node, lineInfo, nodePosition); } // if there is any content after the node on the same line, insert line break (to prevent commenting out extra content) else if (!isWhitespace(endPos, lineInfo.getOffset() + lineInfo.getLength())) { return new InsertEdit(endPos, createLineBreakString(lineInfo.getOffset(), false)); } else { // there is only whitespace on this line - no need for line break return null; } } /** * @param lineStart the first character of the line excluding <code>CR</code> or <code>LF</code> characters. * @return indent of the line starting at <code>lineStart</code>, empty string if there is no indent or <code>lineStart</code> is invalid position * @see IndentManipulation#isIndentChar(char) */ protected String getIndent(int lineStart) { if (lineStart >= 0 && lineStart < charContent.length) { int i = lineStart; while (i < charContent.length && IndentManipulation.isIndentChar(charContent[i])) { i++; } return new String(charContent, lineStart, i - lineStart); } return EMPTY_STRING; } /** * Determines if there are only whitespace characters in the given range in char array. * @param start * @param end * @return <code>true</code> if only whitespace characters are between <code>start</code> and <code>end</code> * * @see Character#isWhitespace(char) */ protected boolean isWhitespace(int start, int end) { if (start >= 0 && end < charContent.length && start <= end) { for (int i = start; i < end; i++) { if (!Character.isWhitespace(charContent[i])) { return false; } } } return true; } /** * Creates line break string containing line delimiter, line comment string if <code>isCommentedOut</code> is <code>true</code>, * and same indent string as the line that break is inserted at. * @param startOfLineOffset * @param isCommentedOut * @return line break string * @throws BadLocationException */ protected String createLineBreakString(int startOfLineOffset, boolean isCommentedOut) throws BadLocationException { return createLineBreakString(getDocument().getLineDelimiter(getDocument().getLineOfOffset(startOfLineOffset)), startOfLineOffset, isCommentedOut); } /** * Creates line break string containing line delimiter, line comment string if <code>isCommentedOut</code> is <code>true</code>, * and indent string that is the same as of the line that break is inserted at. * * @param lineDelimiter * @param startOfLineOffset * @param isCommentedOut * @return line break string */ protected String createLineBreakString(String lineDelimiter, int startOfLineOffset, boolean isCommentedOut) { String indent = getIndent(startOfLineOffset); StringBuilder sb = new StringBuilder(indent.length() + 10); sb.append(lineDelimiter); if (isCommentedOut) { sb.append(LINE_COMMENT_STRING); } sb.append(indent); return sb.toString(); } /** * Creates an InsertEdit that comments out separator after the previous enum constant. * <p> * Separator of the previous constant needs to be commented out when all following constants are commented out. * <p> * For example, if there are constants <code>C1, C2, C3;</code> and constants C2 and C3 are commented out, comma after C1 should be commented out as well. * When this method is called on the first line of C2, the returned edit will comment out a comma after C1. Calling this method on C3 will return <code>null</code> since C2 and C3 are both * commented out. * <p> * Returned edit (if any) may or may not contain a line break depending whether there is any content before the comma on the same line. * * @param enumConstant * @param lineInfo * @param nodePosition * @return <code>InsertEdit</code> or <code>null</code> if none required * @throws BadLocationException */ private InsertEdit commentOutEnumConstantSeparator(EnumConstantDeclaration enumConstant, IRegion lineInfo, ITrackedNodePosition nodePosition) throws BadLocationException { // if previous node is not commented out, but all the following nodes are, comment out a comma (constant separator) ASTJNode<?> astjNode = (ASTJNode<?>)getFacadeHelper().convertToNode(enumConstant); if (astjNode != null) { ASTNode parent = astjNode.getParent().getASTNode(); List<?> enumConstants = rewriter.getListRewrite(parent, EnumDeclaration.ENUM_CONSTANTS_PROPERTY).getRewrittenList(); int constantIndex = enumConstants.indexOf(enumConstant); if (constantIndex > 0 && constantIndex < enumConstants.size()) { ASTNode previousNode = (ASTNode)enumConstants.get(constantIndex - 1); List<?> followingConstants = enumConstants.subList(constantIndex, enumConstants.size()); // if previous node is not commented out, but all following are if (!commentedOutPositions.containsKey(previousNode) && commentedOutPositions.keySet().containsAll(followingConstants)) { // insert new line at the end of previous constant int commaPosition = nodePosition.getStartPosition() - 1; while (commaPosition >=0 && Character.isWhitespace(charContent[commaPosition])) { commaPosition--; } // we should be able to find the comma because the range for enum constants includes all preceding comments up to the previous constant // if TargetSourceRangeComputer of ASTRewrite changes, this logic should change to skip comments // see org.eclipse.emf.codegen.merge.java.facade.ast.CommentAwareSourceRangeComputer#getEnumConstantSourceRange(ASTNode) if (commaPosition >=0 && charContent[commaPosition] == ',') { // if comma is on a line by itself, comment it out but do not insert the line break int line = getDocument().getLineOfOffset(commaPosition); int startOfLineOffset = getDocument().getLineOffset(line); if (isWhitespace(startOfLineOffset, commaPosition)) { return new InsertEdit(startOfLineOffset, LINE_COMMENT_STRING); } else { return new InsertEdit(commaPosition, createLineBreakString(getDocument().getLineDelimiter(line), startOfLineOffset, true)); } } } } } return null; } /** * If needed, creates a line break after enum constant. * * @param node * @param lineInfo * @param nodePosition * @return <code>InsertEdit</code> or <code>null</code> if none required * @throws BadLocationException */ private InsertEdit createLineBreakAfterEnumConstant(ASTNode node, IRegion lineInfo, ITrackedNodePosition nodePosition) throws BadLocationException { int nodeEndPos = nodePosition.getStartPosition() + nodePosition.getLength(); int endOfLine = lineInfo.getOffset() + lineInfo.getLength(); int i = nodeEndPos; // skip whitespace, up to the end of the line while (i <= endOfLine && Character.isWhitespace(charContent[i])) { i++; } // insert line break if there is non-whitespace before end of the line if (i < endOfLine) { // if current char is comma, insert line break after it // // we should be able to find the comma because the range for enum constants includes all trailing comments up to the separator // if TargetSourceRangeComputer of ASTRewrite changes, this logic should change to skip comments // see org.eclipse.emf.codegen.merge.java.facade.ast.CommentAwareSourceRangeComputer#getEnumConstantSourceRange(ASTNode) if (charContent[i] == ',') { // do not insert line break if there is only whitespace after comma if (!isWhitespace(i + 1, endOfLine)) { return new InsertEdit(i + 1, createLineBreakString(lineInfo.getOffset(), false)); } } else { // there is anything else but comma after constant - insert line break return new InsertEdit(i, createLineBreakString(lineInfo.getOffset(), false)); } } // there is only whitespace after enum constant on the same line - do not add line breaks return null; } } } }