/*******************************************************************************
* Copyright 2006, CHISEL Group, University of Victoria, Victoria, BC, Canada.
* 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:
* The Chisel Group, University of Victoria
*******************************************************************************/
package ca.uvic.cs.tagsea.editing;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import ca.uvic.cs.tagsea.core.Tag;
import ca.uvic.cs.tagsea.extraction.TagExtractor;
import ca.uvic.cs.tagsea.parser.ParseNode;
import ca.uvic.cs.tagsea.parser.ParseTree;
import ca.uvic.cs.tagsea.parser.TagParser;
/**
* This is a collection of useful methods for working with Tags and IDocuments.
*
* @author Chris Callendar
*/
public class TagRefactoring {
/**
* Finds the ParseNode in the ParseTree which matches the tag and its hierarchy.
* @param tag
* @param tree
* @return ParseNode or null if the tag wasn't found in the ParseTree
*/
public static ParseNode findParseNodeForTag(Tag tag, ParseTree tree) {
// hierarchy of ParseNodes each having one child except the leaf
ParseTree hierarchy = buildParseTreeUpHierarchy(tag);
ParseNode node = tree.getRoot();
ParseNode hierarchyNode = hierarchy.getRoot();
if (node.hasChildren()) {
while (node.hasChildren() && hierarchyNode.hasChildren()) {
hierarchyNode = hierarchyNode.getChildren()[0];
boolean found = false;
for (ParseNode child : node.getChildren()) {
if (hierarchyNode.getName().equals(child.getName())) {
node = child;
found = true;
break;
}
}
if (!found) {
node = null;
break;
}
}
} else {
node = null;
}
return node;
}
/**
* Creates a ParseTree for the given tag and its ancestors
* up the hierarchy. The children of tag won't be included in the parse tree.
* @param tag the tag
* @return ParseTree containing ParseNodes that each has one child except the leaf node
*/
public static ParseTree buildParseTreeUpHierarchy(Tag tag) {
ParseTree tree = new ParseTree();
ParseNode current = null, child = null;
while ((tag != null) && (tag.getName() != null) && (tag.getName().length() > 0)) {
current = new ParseNode(tag.getName());
if (child != null) {
current.addChild(child);
}
child = current;
tag = tag.getParent();
}
if (current != null) {
tree.getRoot().addChild(current);
}
return tree;
}
/**
* Removes the node from it's parent.
* If the parent has no more children then it is removed from it's parent.
* @param node
* @return boolean if all the nodes were removed, false if some still exist
*/
public static boolean removeParseNodeFromParent(ParseNode node) {
boolean removedAll = true;
if (node != null) {
ParseNode parent = node.getParent();
if (parent != null) {
parent.removeChild(node);
if (parent.hasChildren()) {
removedAll = false;
} else {
removedAll = removeParseNodeFromParent(parent);
}
}
}
return removedAll;
}
/**
* Renames a tag in the given document. Duplicate tag names are condensed into one tag.
* @param tag the tag to rename in the document - this tag's name does not get changed in this method
* @param newTagName the new name for the tag
* @param doc the document
* @param offset the offset in the document where the (@)tag resides
* @param length the length of the tag
* @return boolean if the rename happened
*/
public static boolean renameTagInDocument(Tag tag, String newTagName, IDocument doc, int offset, int length) throws Exception {
if ((tag == null) || (newTagName == null) || (newTagName.length() == 0))
throw new NullPointerException("The tag cannot be null, and the new tag name must be valid.");
if (newTagName.equals(tag.getName()))
return false;
String tagText = doc.get(offset, length); // something like '(@)tag tag1 : comment'
// find the position of the (@)tag
int indexOfAtTag = tagText.indexOf("@tag");
if (indexOfAtTag == -1)
throw new Exception("Failed to find '@tag' in the document at the given offset and length.");
TagParser parser = new TagParser();
ParseTree tree = parser.parse(tagText);
ParseNode foundNode = findParseNodeForTag(tag, tree);
if (foundNode != null) {
String oldKeywords = tree.getKeywords();
if (foundNode.getParent() != null) {
// if parent already contains a child with newTagName then remove it to not have duplicates
foundNode.getParent().removeChild(newTagName);
}
// now update the found ParseNode to have the new name
foundNode.setName(newTagName);
// now regenerate the keywords with the updated tag names
String newKeywords = tree.generateKeywords();
// find the position of the old keywords, starting after the (@)tag
// @tag Bug(166) Refactoring : must start looking after the (@)tag!
int index = tagText.indexOf(oldKeywords, indexOfAtTag + 4);
if (index != -1) {
doc.replace(offset + index, oldKeywords.length(), newKeywords);
return true;
}
}
return false;
}
/**
* Renames a tag in the given document. Duplicate tag names are condensed into one tag.
* @param tag the tag to delete in the document
* @param doc the document
* @param offset the offset in the document where the (@)tag resides
* @param length the length of the tag
* @return boolean if the rename happened
*/
public static boolean deleteTagInDocument(Tag tag, IDocument doc, int offset, int length) throws Exception {
if (tag == null)
throw new NullPointerException("The tag cannot be null.");
String tagText = doc.get(offset, length); // something like '(@)tag tag1 : comment'
// find the position of the (@)tag
int indexOfAtTag = tagText.indexOf("@tag");
if (indexOfAtTag == -1)
throw new Exception("Couldn't find '@tag' in the document at the given offset and length.");
TagParser parser = new TagParser();
ParseTree tree = parser.parse(tagText);
ParseNode nodeToRemove = findParseNodeForTag(tag, tree);
// remove the node, and
if (nodeToRemove != null) {
boolean removedAll = removeParseNodeFromParent(nodeToRemove);
String oldKeywords = tree.getKeywords();
// now regenerate the keywords with the updated tag names
String newKeywords = tree.generateKeywords();
// must start looking after the (@)tag
int index = tagText.indexOf(oldKeywords, indexOfAtTag + 4);
if (index != -1) {
if (!removedAll) {
// there is still at least one tag on this line
doc.replace(offset + index, oldKeywords.length(), newKeywords);
} else {
// there are no tags on this line anymore, remove them all
doc.replace(offset, tagText.length(), "");
removeEmptyComment(doc, offset, length - tagText.length());
}
return true;
}
} else {
throw new Exception("Couldn't find the node to remove at the given offset and length");
}
return false;
}
/**
* Moves a tag in the given document.
* @param tag the tag to rename in the document - this tag's name does not get changed in this method
* @param newParent the new parent for tag - can be to make the tag become a root node
* @param doc the document
* @param offset the offset in the document where the (@)tag resides
* @param length the length of the tag
* @return boolean if the move happened
* @throws Exception if tag is null, if the document doesn't contain (@)tag
*/
public static boolean moveTagInDocument(Tag tag, Tag newParent, IDocument doc, int offset, int length) throws Exception {
if (tag == null)
throw new NullPointerException("The tag cannot be null.");
// don't do anything if already have the same parents
if (tag.getParent() == newParent)
return false;
// don't do anything if tag equals newParent or is a parent of newParent
Tag compare = newParent;
while (compare != null) {
if (compare == tag) {
return false;
}
compare = compare.getParent();
}
String tagText = doc.get(offset, length); // something like '(@)tag tag1 : comment'
// find the position of the (@)tag
int indexOfAtTag = tagText.indexOf("@tag");
if (indexOfAtTag == -1)
throw new Exception("Couldn't find '@tag' in the document at the given offset and length.");
TagParser parser = new TagParser();
ParseTree tree = parser.parse(tagText);
String oldKeywords = tree.getKeywords();
// find the parse node for the given tag
ParseNode node = findParseNodeForTag(tag, tree);
if (node != null) {
removeParseNodeFromParent(node);
// now build a new parse node tree for the parent
if (newParent == null) {
tree.getRoot().addChild(node);
} else {
ParseTree pt = buildParseTreeUpHierarchy(newParent);
ParseNode parentNode = addParseNodeHierarchyToTree(tree, pt);
parentNode.addChild(node);
}
// now regenerate the keywords with the updated tag names
String newKeywords = tree.generateKeywords();
// must start looking after the (@)tag
int index = tagText.indexOf(oldKeywords, indexOfAtTag + 4);
if (index != -1) {
doc.replace(offset + index, oldKeywords.length(), newKeywords);
return true;
}
}
return false;
}
/**
* For debugging. Prints out the parse tree.
* @param tree
*/
@SuppressWarnings("restriction")
public static void printParseTree(ParseTree tree) {
ParseNode root = tree.getRoot();
System.out.println((root.getName() == null ? "ROOT" : root.getName()));
printParseTree(root.getChildren(), " ");
}
/**
* For debugging. Prints out the parse node and it's descendents.
* @param node
*/
@SuppressWarnings("restriction")
public static void printParseTree(ParseNode node) {
System.out.println((node.getName() == null ? "ROOT" : node.getName()));
printParseTree(node.getChildren(), " ");
}
@SuppressWarnings("restriction")
private static void printParseTree(ParseNode[] children, String spc) {
for (ParseNode child : children) {
if (child.hasChildren()) {
System.out.println(spc + "+ " + child.getName());
printParseTree(child.getChildren(), spc + " ");
} else {
System.out.println(spc + "- " + child.getName());
}
}
}
/**
* Adds the given node and its children to the parse tree.
* @param realTree the ParseTree which will have ParseNodes added to it from the treeHierarchy ParseTree.
* @param treeHierarchy the single hierarchy of ParseNodes which all only <b>one</b> child except for the leaf node.
* These will be added to the realTree ParseTree.
* @return ParseNode the added (or already existing) <b>leaf</b> ParseNode from the ParseTree
*/
public static ParseNode addParseNodeHierarchyToTree(ParseTree realTree, ParseTree treeHierarchy) {
ParseNode leaf = null;
ParseNode[] root = treeHierarchy.getNodeCollection();
if (root.length == 1) {
leaf = addParseNodeToParent(realTree.getRoot(), root[0]);
}
return leaf;
}
/**
* Adds the given node and its children to the parent ParseNode.
* @param parent the parent ParseNode
* @param node the node to add (single hierarchy)
* @return ParseNode the added (or already existing) <b>leaf</b> ParseNode
*/
private static ParseNode addParseNodeToParent(ParseNode parent, ParseNode node) {
String name = node.getName();
if (name == null)
return null;
ParseNode addedNode = null;
// try to find a ParseNode with the same name
for (ParseNode pn : parent.getChildren()) {
if (name.equals(pn.getName())) {
if (node.hasChildren()) {
addedNode = addParseNodeToParent(pn, node.getChildren()[0]);
} else {
// already exists
addedNode = pn;
}
}
}
if (addedNode == null) {
addedNode = parent.addChild(name);
if (node.hasChildren()) {
addedNode = addParseNodeToParent(addedNode, node.getChildren()[0]);
}
}
return addedNode;
}
/**
* Uses the TagExtractor to get the comment from the document at the given offset and length.
* If there is nothing but whitespace inside the comment, it is removed.
* @param doc the document containing the comment
* @param offset the offset where the comment is
* @param length the length of the comment
* @return boolean if the comment was empty and was removed
* @throws Exception
*/
public static boolean removeEmptyComment(IDocument doc, final int offset, int length) throws Exception {
// now get the comment and check if anything is left behind, if not remove it too
IRegion[] comments = TagExtractor.getCommentRegions(doc, 0, doc.getLength());
if (comments.length == 0)
return false;
int commentOffset = offset;
boolean foundComment = false;
for (IRegion region : comments) {
int regionOffset = region.getOffset();
int regionLength = region.getLength();
if ((regionOffset < offset) && ((regionOffset + regionLength) >= (offset + length))) {
commentOffset = regionOffset;
length = regionLength;
foundComment = true;
break;
}
}
if (!foundComment)
return false;
String comment = doc.get(commentOffset, length);
String trimmed = comment.trim();
boolean singleLine = trimmed.startsWith("//");
boolean multiLine = trimmed.startsWith("/*") && trimmed.endsWith("*/");
//boolean javaDoc = multiLine && trimmed.startsWith("/**");
// remove the comment characters
if (singleLine) {
trimmed = trimmed.substring(2);
} else if (multiLine) {
trimmed = trimmed.substring(2, trimmed.length() - 4);
//if (javaDoc) {
// note this also removes any '*' that appear in the java doc comment...
trimmed = trimmed.replace('*', ' ');
//}
}
// now remove any extra whitespace
trimmed = trimmed.trim();
if (trimmed.length() == 0) {
// since the comment is only whitespace, remove it entirely
// check if the text before the comment on the same line if not whitespace
int startOfLine = doc.getLineOffset(doc.getLineOfOffset(commentOffset));
if (commentOffset > startOfLine) {
String beforeComment = doc.get(startOfLine, commentOffset - startOfLine).trim();
// adjust the offset to the start of the line to remove the line altogether
if (beforeComment.length() == 0) {
length = length + (commentOffset - startOfLine);
commentOffset = startOfLine;
}
}
// now replace the comment with the empty string
doc.replace(commentOffset, length, "");
return true;
}
// if the whole comment wasn't remove - check if a multiline comment left a blank line
if (multiLine) {
int line = doc.getLineOfOffset(offset);
int startOfLine = doc.getLineOffset(line);
int lineLength = doc.getLineLength(line); // includes newline delimeters
String blankLine = doc.get(startOfLine, lineLength);
blankLine = blankLine.replace('*', ' ').trim();
if (blankLine.length() == 0) {
// empty line, so remove
doc.replace(startOfLine, lineLength, "");
}
}
return false;
}
}