/*******************************************************************************
* Copyright (c) 2008 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 (Alex Panchenko)
* Aptana Inc. - Modify it to work with org.jrubyparser.parser AST (Shalom Gibly)
*******************************************************************************/
package com.aptana.editor.ruby.formatter.internal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Pattern;
import org.jrubyparser.ISourcePositionHolder;
import org.jrubyparser.SourcePosition;
import org.jrubyparser.ast.ArgumentNode;
import org.jrubyparser.ast.ArrayNode;
import org.jrubyparser.ast.BeginNode;
import org.jrubyparser.ast.BlockNode;
import org.jrubyparser.ast.CaseNode;
import org.jrubyparser.ast.ClassNode;
import org.jrubyparser.ast.Colon3Node;
import org.jrubyparser.ast.ConstNode;
import org.jrubyparser.ast.DRegexpNode;
import org.jrubyparser.ast.DStrNode;
import org.jrubyparser.ast.DefnNode;
import org.jrubyparser.ast.DefsNode;
import org.jrubyparser.ast.EnsureNode;
import org.jrubyparser.ast.FCallNode;
import org.jrubyparser.ast.ForNode;
import org.jrubyparser.ast.HashNode;
import org.jrubyparser.ast.IfNode;
import org.jrubyparser.ast.InstAsgnNode;
import org.jrubyparser.ast.IterNode;
import org.jrubyparser.ast.ListNode;
import org.jrubyparser.ast.Match2Node;
import org.jrubyparser.ast.Match3Node;
import org.jrubyparser.ast.MatchNode;
import org.jrubyparser.ast.MethodDefNode;
import org.jrubyparser.ast.ModuleNode;
import org.jrubyparser.ast.NilImplicitNode;
import org.jrubyparser.ast.NilNode;
import org.jrubyparser.ast.Node;
import org.jrubyparser.ast.NodeType;
import org.jrubyparser.ast.OptArgNode;
import org.jrubyparser.ast.PostExeNode;
import org.jrubyparser.ast.PreExeNode;
import org.jrubyparser.ast.RegexpNode;
import org.jrubyparser.ast.RescueBodyNode;
import org.jrubyparser.ast.RescueNode;
import org.jrubyparser.ast.ReturnNode;
import org.jrubyparser.ast.SClassNode;
import org.jrubyparser.ast.StrNode;
import org.jrubyparser.ast.SymbolNode;
import org.jrubyparser.ast.UntilNode;
import org.jrubyparser.ast.VCallNode;
import org.jrubyparser.ast.WhenNode;
import org.jrubyparser.ast.WhileNode;
import org.jrubyparser.ast.XStrNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterArrayNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterAtBeginNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterAtEndNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterBeginNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterCaseNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterClassNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterDoNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterElseIfNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterEnsureNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterForNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterHashNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterIfElseNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterIfEndNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterIfNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterMethodNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterModuleNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterRequireNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterRescueElseNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterRescueNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterStringNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterUntilNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterWhenElseNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterWhenNode;
import com.aptana.editor.ruby.formatter.internal.nodes.FormatterWhileNode;
import com.aptana.formatter.IFormatterDocument;
import com.aptana.formatter.nodes.AbstractFormatterNodeBuilder;
import com.aptana.formatter.nodes.IFormatterContainerNode;
import com.aptana.formatter.nodes.IFormatterTextNode;
import com.aptana.ruby.core.ast.AbstractVisitor;
/**
* This class builds the nodes by visiting the Ruby AST tree.
*/
public class RubyFormatterNodeBuilderVisitor extends AbstractVisitor
{
private IFormatterDocument document;
private RubyFormatterNodeBuilder builder;
private static final Pattern LINE_SPLIT_PATTERN = Pattern.compile("\r?\n|\r"); //$NON-NLS-1$
protected RubyFormatterNodeBuilderVisitor(IFormatterDocument document, RubyFormatterNodeBuilder builder)
{
this.document = document;
this.builder = builder;
}
protected Object visitNode(Node visited)
{
visitChildren(visited);
return null;
}
public Object visitClassNode(ClassNode visited)
{
FormatterClassNode classNode = new FormatterClassNode(document);
SourcePosition position = visited.getPosition();
classNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document, position.getStartOffset(), visited
.getCPath().getPosition().getStartOffset()));
builder.push(classNode);
// visitChildren(visited);
Node bodyNode = visited.getBodyNode();
int bodyEndOffset;
if (NodeType.NILNODE.equals(bodyNode.getNodeType()))
{
bodyEndOffset = classNode.getEndOffset();
}
else
{
bodyNode.accept(this);
bodyEndOffset = bodyNode.getPosition().getEndOffset();
}
builder.checkedPop(classNode, bodyEndOffset);
classNode.setEnd(AbstractFormatterNodeBuilder.createTextNode(document, bodyEndOffset, position.getEndOffset()));
return null;
}
public Object visitSClassNode(SClassNode visited)
{
FormatterClassNode classNode = new FormatterClassNode(document);
SourcePosition position = visited.getPosition();
classNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document, position.getStartOffset(), visited
.getReceiverNode().getPosition().getStartOffset()));
builder.push(classNode);
visitChildren(visited);
Node bodyNode = visited.getBodyNode();
Node receiverNode = visited.getReceiverNode();
int bodyEndOffset = position.getEndOffset();
if (bodyNode != null)
{
bodyEndOffset = bodyNode.getPosition().getEndOffset();
}
else if (receiverNode != null)
{
bodyEndOffset = receiverNode.getPosition().getEndOffset();
}
builder.checkedPop(classNode, bodyEndOffset);
classNode.setEnd(AbstractFormatterNodeBuilder.createTextNode(document, bodyEndOffset, position.getEndOffset()));
return null;
}
public Object visitModuleNode(ModuleNode visited)
{
FormatterModuleNode moduleNode = new FormatterModuleNode(document);
Colon3Node pathNode = visited.getCPath();
moduleNode.setBegin(createTextNode(document, pathNode));
builder.push(moduleNode);
visitChildren(visited);
SourcePosition position = visited.getPosition();
Node bodyNode = visited.getBodyNode();
int bodyEndOffset;
if (bodyNode instanceof NilImplicitNode)
{
// empty 'module' body
bodyEndOffset = moduleNode.getEndOffset();
}
else
{
bodyEndOffset = bodyNode.getPosition().getEndOffset();
}
builder.checkedPop(moduleNode, bodyEndOffset);
moduleNode
.setEnd(AbstractFormatterNodeBuilder.createTextNode(document, bodyEndOffset, position.getEndOffset()));
return null;
}
public Object visitDefnNode(DefnNode visited)
{
return visitMethodDefNode(visited);
}
public Object visitDefsNode(DefsNode visited)
{
return visitMethodDefNode(visited);
}
private Object visitMethodDefNode(MethodDefNode visited)
{
FormatterMethodNode methodNode = new FormatterMethodNode(document);
SourcePosition position = visited.getPosition();
methodNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document, position.getStartOffset(), visited
.getNameNode().getPosition().getEndOffset()));
builder.push(methodNode);
visitChildren(visited);
Node bodyNode = visited.getBodyNode();
int bodyEndOffset;
if (bodyNode != null)
{
bodyEndOffset = bodyNode.getPosition().getEndOffset();
}
else
{
bodyEndOffset = locateEndOffset(document, position.getEndOffset());
}
builder.checkedPop(methodNode, bodyEndOffset);
methodNode
.setEnd(AbstractFormatterNodeBuilder.createTextNode(document, bodyEndOffset, position.getEndOffset()));
return null;
}
public Object visitWhileNode(WhileNode visited)
{
FormatterWhileNode whileNode = new FormatterWhileNode(document);
SourcePosition position = visited.getPosition();
Node conditionNode = visited.getConditionNode();
whileNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document, position.getStartOffset(),
conditionNode.getPosition().getEndOffset()));
builder.push(whileNode);
Node bodyNode = visited.getBodyNode();
visitChildren(bodyNode);
int bodyEndOffset;
if (bodyNode instanceof NilImplicitNode)
{
// empty 'while' body
bodyEndOffset = whileNode.getEndOffset();
}
else
{
bodyEndOffset = bodyNode.getPosition().getEndOffset();
}
builder.checkedPop(whileNode, bodyEndOffset);
whileNode.setEnd(AbstractFormatterNodeBuilder.createTextNode(document, bodyEndOffset, position.getEndOffset()));
return null;
}
public Object visitIterNode(IterNode visited)
{
FormatterDoNode forNode = new FormatterDoNode(document);
// Note: The iteration may or may not have a 'varNode'. Also, it may or may not have a 'body'.
Node varNode = visited.getVarNode();
Node bodyNode = visited.getBodyNode();
SourcePosition position = visited.getPosition();
boolean isCurlyIteration = document.charAt(position.getStartOffset()) == '{';
// Here we try to figure out what to place in the begin segment
int beginEndOffset = position.getEndOffset();
if (bodyNode != null)
{
beginEndOffset = bodyNode.getPosition().getStartOffset();
}
else
{
// we have an iteration without a body.
if (varNode != null)
{
// we look for the start offset of the 'end'
// keyword, right after the var-node
beginEndOffset = varNode.getPosition().getEndOffset();
if (isCurlyIteration)
{
// we need to locate the ending curly
if (document.charAt(beginEndOffset - 1) != '}')
{
// cover a case like:
// iteration {|x|
// }
beginEndOffset = position.getEndOffset() - 1;
}
}
else
{
// we need to locate the starting of the 'end' keyword
int endKeywordStart = charLookup(document, beginEndOffset, 'e');
if (endKeywordStart > -1)
{
beginEndOffset = endKeywordStart;
}
}
}
else
{
// we have no body-node, nor var-node.
// in this case we just look for the start of the 'end' keyword.
beginEndOffset = locateEndOffset(document, position.getEndOffset());
}
}
forNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document, position.getStartOffset(),
beginEndOffset));
builder.push(forNode);
visitChildren(visited);
int bodyEndOffset;
if (bodyNode != null)
{
bodyEndOffset = bodyNode.getPosition().getEndOffset();
}
else
{
bodyEndOffset = forNode.getEndOffset();
}
builder.checkedPop(forNode, bodyEndOffset);
forNode.setEnd(AbstractFormatterNodeBuilder.createTextNode(document, bodyEndOffset,
Math.max(bodyEndOffset, position.getEndOffset())));
return null;
}
public Object visitForNode(ForNode visited)
{
FormatterForNode forNode = new FormatterForNode(document);
Node bodyNode = visited.getBodyNode();
Node iterNode = visited.getIterNode();
int bodyStart = -1;
int bodyEnd = -1;
SourcePosition position = visited.getPosition();
if (iterNode != null)
{
bodyStart = iterNode.getPosition().getEndOffset();
}
if (bodyNode != null)
{
if (bodyStart < 0)
{
bodyStart = locateEndOffset(document, position.getEndOffset());
}
bodyEnd = bodyNode.getPosition().getEndOffset() - 1;
}
else
{
if (bodyStart < 0)
{
bodyStart = locateEndOffset(document, position.getEndOffset());
}
if (bodyEnd < 0)
{
bodyEnd = bodyStart;
}
}
forNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document, position.getStartOffset(), bodyStart));
builder.push(forNode);
if (bodyNode != null)
{
bodyNode.accept(this);
}
builder.checkedPop(forNode, bodyEnd);
forNode.setEnd(AbstractFormatterNodeBuilder.createTextNode(document, bodyEnd, position.getEndOffset()));
return null;
}
public Object visitUntilNode(UntilNode visited)
{
FormatterUntilNode untilNode = new FormatterUntilNode(document);
untilNode.setBegin(createTextNode(document, visited));
builder.push(untilNode);
visitChildren(visited);
SourcePosition position = visited.getPosition();
Node bodyNode = visited.getBodyNode();
int bodyEndOffset = bodyNode.getPosition().getEndOffset();
builder.checkedPop(untilNode, bodyEndOffset);
untilNode.setEnd(AbstractFormatterNodeBuilder.createTextNode(document, bodyEndOffset, position.getEndOffset()));
return null;
}
public Object visitCaseNode(CaseNode visited)
{
FormatterCaseNode caseNode = new FormatterCaseNode(document);
SourcePosition position = visited.getPosition();
caseNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document, position.getStartOffset(), visited
.getCaseNode().getPosition().getEndOffset()));
builder.push(caseNode);
Node branch = visited.getFirstWhenNode();
List<Node> children;
if (branch instanceof ArrayNode)
{
children = branch.childNodes();
}
else
{
children = new ArrayList<Node>();
children.add(branch);
}
int bodyEndOffset = position.getEndOffset();
for (Node child : children)
{
if (child instanceof WhenNode)
{
WhenNode whenBranch = (WhenNode) child;
FormatterWhenNode whenNode = new FormatterWhenNode(document);
SourcePosition branchPosition = child.getPosition();
whenNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document,
branchPosition.getStartOffset(), whenBranch.getExpressionNodes().getPosition().getEndOffset()));
builder.push(whenNode);
Node whenBodyNode = whenBranch.getBodyNode();
visitChild(whenBodyNode);
builder.checkedPop(whenNode, whenBodyNode.getPosition().getEndOffset());
bodyEndOffset = whenNode.getEndOffset();
}
else
{
FormatterWhenElseNode whenElseNode = new FormatterWhenElseNode(document);
SourcePosition elsePosition = child.getPosition();
whenElseNode.setBegin(createTextNode(document, child));
builder.push(whenElseNode);
visitChildren(child.childNodes());
builder.checkedPop(whenElseNode, elsePosition.getEndOffset());
bodyEndOffset = whenElseNode.getEndOffset();
}
}
builder.checkedPop(caseNode, bodyEndOffset);
caseNode.setEnd(AbstractFormatterNodeBuilder.createTextNode(document, bodyEndOffset, position.getEndOffset()));
return null;
}
public Object visitIfNode(IfNode visited)
{
SourcePosition position = visited.getPosition();
if (isInSameLineExcludingWhitespaces(position))
{
// Inline if
List<Node> children = new ArrayList<Node>(3);
if (visited.getThenBody() != null)
{
children.add(visited.getThenBody());
}
if (visited.getElseBody() != null)
{
children.add(visited.getElseBody());
}
if (visited.getCondition() != null)
{
children.add(visited.getCondition());
}
if (!children.isEmpty())
{
Collections.sort(children, POSITION_COMPARATOR);
visitChildren(children);
}
return null;
}
FormatterIfNode ifNode = new FormatterIfNode(document);
ifNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document, position.getStartOffset(), visited
.getCondition().getPosition().getEndOffset()));
builder.push(ifNode);
Node thenBody = visited.getThenBody();
Node elseBody = visited.getElseBody();
if (thenBody instanceof ReturnNode || elseBody instanceof ReturnNode)
{
// we have a special case of 'return if x' or 'return unless x' expression
builder.checkedPop(ifNode, ifNode.getEndOffset());
return null;
}
// Flip the 'else' and 'then' in case the 'then' appears 'after the 'else'
// This is the case with 'unless', so we flip it to make it easier to handle.
int ifNodeEnd = -1; // the end position for the entire if block.
if (elseBody != null)
{
ifNodeEnd = elseBody.getPosition().getEndOffset();
}
else if (thenBody != null)
{
ifNodeEnd = thenBody.getPosition().getEndOffset();
}
if (elseBody != null && thenBody != null)
{
if (thenBody.getPosition().getStartOffset() > elseBody.getPosition().getStartOffset())
{
// we also need to update the end position of the entire if block
ifNodeEnd = thenBody.getPosition().getEndOffset();
// flip the 'the' and 'else'
Node temp = thenBody;
thenBody = elseBody;
elseBody = temp;
}
}
visitChild(thenBody);
if (thenBody == null && elseBody != null)
{
// We have an 'unless' case, so we just visit the else-boby
visitChild(elseBody);
}
builder.checkedPop(ifNode, ifNode.getEndOffset());
while (elseBody != null && thenBody != null)
{
if (elseBody instanceof IfNode)
{
// elsif
IfNode elseIfBranch = (IfNode) elseBody;
FormatterElseIfNode elseIfNode = new FormatterElseIfNode(document);
elseIfNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document, thenBody.getPosition()
.getEndOffset(), elseIfBranch.getCondition().getPosition().getEndOffset()));
builder.push(elseIfNode);
thenBody = elseIfBranch.getThenBody();
visitChild(thenBody);
elseBody = elseIfBranch.getElseBody();
builder.checkedPop(elseIfNode, elseIfNode.getEndOffset());
}
else
{
// else
FormatterIfElseNode elseNode = new FormatterIfElseNode(document);
elseNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document, thenBody.getPosition()
.getEndOffset(), elseBody.getPosition().getStartOffset()));
builder.push(elseNode);
visitChild(elseBody);
builder.checkedPop(elseNode, elseNode.getEndOffset());
elseBody = null;
}
}
if (ifNodeEnd < 0)
{
// locate the 'end'
ifNodeEnd = locateEndOffset(document, position.getEndOffset());
}
builder.addChild(new FormatterIfEndNode(document, ifNodeEnd, position.getEndOffset()));
return null;
}
/*
* (non-Javadoc)
* @see com.aptana.editor.ruby.parsing.ast.AbstractVisitor#visitBeginNode(org.jrubyparser.ast.BeginNode)
*/
public Object visitBeginNode(BeginNode visited)
{
/**
* TODO -NEED TO SUPPORT THIS
*
* <pre>
* begin
* # main code here
* rescue SomeException
* # ...
* rescue AnotherException
* # ..
* else
* # stuff you want to happen AFTER the main code,
* # but BEFORE the ensure block, but only if there
* # were no exceptions raised. Note, too, that
* # exceptions raised here won't be rescued by the
* # rescue clauses above.
* ensure
* # stuff that should happen dead last, and
* # regardless of whether any exceptions were
* # raised or not.
* end
* </pre>
*/
FormatterBeginNode beginNode = new FormatterBeginNode(document);
SourcePosition beginPosition = visited.getPosition();
// We first need to find the body-node of the begin section.
// It can be right under the BeginNode, but in cases where we have
// A RescueNode or an EnsureNode, it will be under those nodes.
Node bodyNode = visited.getBodyNode();
RescueBodyNode rescueBodyNode = null;
int beginEndOffset = -1;
if (bodyNode instanceof EnsureNode)
{
EnsureNode ensureNode = (EnsureNode) bodyNode;
bodyNode = ensureNode.getBodyNode();
Node innerEnsureNode = ensureNode.getEnsureNode();
if (!(innerEnsureNode instanceof NilImplicitNode))
{
beginEndOffset = innerEnsureNode.getPosition().getEndOffset();
}
}
if (bodyNode instanceof RescueNode)
{
RescueNode rescueNode = (RescueNode) bodyNode;
bodyNode = rescueNode.getBodyNode();
rescueBodyNode = rescueNode.getRescueNode();
}
if (bodyNode instanceof NilImplicitNode)
{
// we have an emply body here
if (beginEndOffset < 0)
{
beginEndOffset = locateEndOffset(document, beginPosition.getEndOffset());
}
beginNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document, beginPosition.getStartOffset(),
beginEndOffset));
builder.push(beginNode);
builder.checkedPop(beginNode, beginEndOffset);
beginNode.setEnd(AbstractFormatterNodeBuilder.createTextNode(document, beginEndOffset,
beginPosition.getEndOffset()));
}
else
{
SourcePosition bodyNodePosition = bodyNode.getPosition();
if (beginEndOffset < 0)
{
if (rescueBodyNode instanceof RescueBodyNode)
{
Node innerBodyNode = rescueBodyNode.getBodyNode();
if (!(innerBodyNode instanceof NilImplicitNode))
{
beginEndOffset = innerBodyNode.getPosition().getEndOffset();
}
else
{
beginEndOffset = rescueBodyNode.getPosition().getEndOffset();
}
}
else
{
beginEndOffset = bodyNode.getPosition().getEndOffset();
}
}
beginNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document, beginPosition.getStartOffset(),
bodyNodePosition.getStartOffset()));
builder.push(beginNode);
visitChild(visited.getBodyNode());
builder.checkedPop(beginNode, -1);
beginNode.setEnd(AbstractFormatterNodeBuilder.createTextNode(document, beginNode.getEndOffset(),
beginPosition.getEndOffset()));
}
return null;
}
public Object visitRescueNode(RescueNode visited)
{
visitChild(visited.getBodyNode());
RescueBodyNode rescueBody = visited.getRescueNode();
int lastBodyEndOffset = -1;
while (rescueBody != null)
{
FormatterRescueNode rescueNode = new FormatterRescueNode(document);
Node bodyInnerNode = rescueBody.getBodyNode();
int rescueBeginEndOffset = -1;
SourcePosition position = rescueBody.getPosition();
if (!(bodyInnerNode instanceof NilImplicitNode))
{
SourcePosition innerBodyPosition = bodyInnerNode.getPosition();
rescueBeginEndOffset = innerBodyPosition.getStartOffset();
lastBodyEndOffset = innerBodyPosition.getEndOffset();
}
else
{
// try to look into the exception nodes
Node exceptionNodes = rescueBody.getExceptionNodes();
if (exceptionNodes != null)
{
rescueBeginEndOffset = exceptionNodes.getPosition().getEndOffset();
}
else if (visited.getElseNode() != null)
{
rescueBeginEndOffset = visited.getElseNode().getPosition().getStartOffset();
}
else
{
// FIXME - This will probably fail if we have nested rescue blocks
rescueBeginEndOffset = document.get(position.getStartOffset(), position.getEndOffset())
.lastIndexOf("rescue") + position.getStartOffset() + 6; //$NON-NLS-1$
}
lastBodyEndOffset = rescueBeginEndOffset;
}
rescueNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document, position.getStartOffset(),
rescueBeginEndOffset));
builder.push(rescueNode);
visitChild(rescueBody.getBodyNode());
rescueBody = rescueBody.getOptRescueNode();
final int rescueEnd;
if (rescueBody != null)
{
rescueEnd = position.getStartOffset();
}
else if (visited.getElseNode() != null)
{
rescueEnd = lastBodyEndOffset;
}
else
{
rescueEnd = rescueNode.getEndOffset();
}
builder.checkedPop(rescueNode, rescueEnd);
}
if (visited.getElseNode() != null)
{
final Node elseBranch = visited.getElseNode();
FormatterRescueElseNode elseNode = new FormatterRescueElseNode(document);
elseNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document, lastBodyEndOffset, elseBranch
.getPosition().getStartOffset()));
builder.push(elseNode);
visitChildren(elseBranch.childNodes());
builder.checkedPop(elseNode, -1);
}
return null;
}
public Object visitEnsureNode(EnsureNode visited)
{
Node bodyNode = visited.getBodyNode();
visitChild(bodyNode);
FormatterEnsureNode ensureNode = new FormatterEnsureNode(document);
Node node = visited.getEnsureNode();
SourcePosition position = visited.getPosition();
// FIXME - This will probably fail if we have nested rescue blocks
int ensureStartOffset = document.get(position.getStartOffset(), position.getEndOffset()).lastIndexOf("ensure") //$NON-NLS-1$
+ position.getStartOffset();
ensureNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document, ensureStartOffset, node.getPosition()
.getStartOffset()));
builder.push(ensureNode);
visitChildren(node.childNodes());
builder.checkedPop(ensureNode, -1);
return null;
}
public Object visitPreExeNode(PreExeNode visited)
{
FormatterAtBeginNode endNode = new FormatterAtBeginNode(document);
endNode.setBegin(createTextNode(document, visited));
builder.push(endNode);
visitChildren(visited);
SourcePosition position = visited.getPosition();
Node bodyNode = visited.getBodyNode();
int bodyEndOffset = bodyNode.getPosition().getEndOffset();
builder.checkedPop(endNode, bodyEndOffset);
endNode.setEnd(AbstractFormatterNodeBuilder.createTextNode(document, bodyEndOffset, position.getEndOffset()));
return null;
}
public Object visitPostExeNode(PostExeNode visited)
{
FormatterAtEndNode endNode = new FormatterAtEndNode(document);
endNode.setBegin(createTextNode(document, visited));
builder.push(endNode);
visitChildren(visited);
SourcePosition position = visited.getPosition();
Node bodyNode = visited.getBodyNode();
int bodyEndOffset = bodyNode.getPosition().getEndOffset();
builder.checkedPop(endNode, bodyEndOffset);
endNode.setEnd(AbstractFormatterNodeBuilder.createTextNode(document, bodyEndOffset, position.getEndOffset()));
return null;
}
public Object visitNilNode(NilNode visited)
{
pushTextNode(visited.getPosition());
return null;
}
public Object visitConstNode(ConstNode visited)
{
pushTextNode(visited.getPosition());
return null;
}
/*
* (non-Javadoc)
* @see com.aptana.ruby.core.ast.AbstractVisitor#visitSymbolNode(org.jrubyparser.ast.SymbolNode)
*/
@Override
public Object visitSymbolNode(SymbolNode visited)
{
pushTextNode(visited.getPosition());
return null;
}
public Object visitStrNode(StrNode visited)
{
pushTextNode(visited.getPosition());
return null;
}
public Object visitVCallNode(VCallNode visited)
{
pushTextNode(visited.getPosition());
return null;
}
public Object visitDStrNode(DStrNode visited)
{
pushTextNode(visited.getPosition());
return null;
}
public Object visitMatch2Node(Match2Node visited)
{
SourcePosition position = visited.getPosition();
SourcePosition receiverPosition = visited.getReceiverNode().getPosition();
pushTextNode(position.getStartOffset(), receiverPosition.getEndOffset());
return null;
}
public Object visitMatch3Node(Match3Node visited)
{
visitNode(visited.getValueNode());
visitNode(visited.getReceiverNode());
return null;
}
public Object visitMatchNode(MatchNode visited)
{
SourcePosition position = visited.getPosition();
SourcePosition receiverPosition = visited.getRegexpNode().getPosition();
pushTextNode(position.getStartOffset(), receiverPosition.getEndOffset());
return null;
}
public Object visitRegexpNode(RegexpNode visited)
{
pushTextNode(visited.getPosition());
return null;
}
public Object visitDRegxNode(DRegexpNode visited)
{
pushTextNode(visited.getPosition());
return null;
}
public Object visitXStrNode(XStrNode visited)
{
pushTextNode(visited.getPosition());
return null;
}
public Object visitFCallNode(FCallNode visited)
{
if (isRequireMethod(visited))
{
SourcePosition position = visited.getPosition();
FormatterRequireNode requireNode = new FormatterRequireNode(document, position.getStartOffset(),
position.getEndOffset());
builder.addChild(requireNode);
}
else if (visited.getIterNode() != null)
{
// it's a block iteration node
int iterStart = visited.getIterNode().getPosition().getStartOffset();
pushTextNode(visited.getPosition().getStartOffset(), iterStart);
visitChild(visited.getIterNode());
}
else
{
pushTextNode(visited.getPosition());
}
return null;
}
public Object visitArrayNode(ArrayNode visited)
{
IFormatterContainerNode containerNode = builder.peek();
if (containerNode.getStartOffset() == visited.getPosition().getStartOffset())
{
// just analyze the children. This is an ArrayNode inside an ArrayNode
visitChildren(visited);
return null;
}
SourcePosition position = visited.getPosition();
String right = document.get(position.getStartOffset(), position.getStartOffset() + 1);
String left = document.get(position.getEndOffset() - 1, position.getEndOffset());
if ("[".equals(right) && "]".equals(left)) { //$NON-NLS-1$ //$NON-NLS-2$
final FormatterArrayNode arrayNode = new FormatterArrayNode(document);
arrayNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document, position.getStartOffset(),
position.getStartOffset() + 1));
builder.push(arrayNode);
visitChildren(visited);
builder.checkedPop(arrayNode, position.getEndOffset() - 1);
arrayNode.setEnd(AbstractFormatterNodeBuilder.createTextNode(document, position.getEndOffset() - 1,
position.getEndOffset()));
return null;
}
else
{
// we should probably better handle ArrayNodes that arrive without the brackets.
// For example:
// job = Delayed::Job.find :first,
// :order => "run_at ASC"
return super.visitArrayNode(visited);
}
}
public Object visitHashNode(HashNode visited)
{
SourcePosition position = visited.getPosition();
String left = document.get(position.getStartOffset(),
Math.min(position.getStartOffset() + 1, document.getLength()));
String right = document.get(Math.max(0, position.getEndOffset() - 1), position.getEndOffset());
if ("{".equals(left) && "}".equals(right)) { //$NON-NLS-1$ //$NON-NLS-2$
final FormatterHashNode hashNode = new FormatterHashNode(document);
hashNode.setBegin(AbstractFormatterNodeBuilder.createTextNode(document, position.getStartOffset(),
position.getStartOffset() + 1));
builder.push(hashNode);
// builder.checkedPop(hashNode, right.getStartOffset());
visitChildren(visited);
builder.checkedPop(hashNode, position.getEndOffset() - 1);
hashNode.setEnd(AbstractFormatterNodeBuilder.createTextNode(document, position.getEndOffset() - 1,
position.getEndOffset()));
return null;
}
else
{
return super.visitHashNode(visited);
}
}
/*
* (non-Javadoc)
* @see com.aptana.ruby.core.ast.AbstractVisitor#visitBlockNode(org.jrubyparser.ast.BlockNode)
*/
@Override
public Object visitBlockNode(BlockNode visited)
{
visitChildren(visited.childNodes());
return null;
}
/*
* (non-Javadoc)
* @see com.aptana.ruby.core.ast.AbstractVisitor#visitInstAsgnNode(org.jrubyparser.ast.InstAsgnNode)
*/
@Override
public Object visitInstAsgnNode(InstAsgnNode visited)
{
pushTextNode(visited.getPosition());
return null;
}
private boolean isRequireMethod(FCallNode call)
{
if ("require".equals(call.getName())) //$NON-NLS-1$
{
if (call.getArgsNode() instanceof ArrayNode)
{
return true;
}
}
return false;
}
private void visitChildren(Node visited)
{
final List<Node> children = visited.childNodes();
if (!children.isEmpty())
{
visitChildren(children);
}
}
private void visitChildren(List<Node> children)
{
for (Node child : children)
{
visitChild(child);
}
}
private void visitChild(final Node child)
{
if (child != null && isVisitable(child))
{
try
{
child.accept(this);
}
catch (UnsupportedOperationException e)
{
if (child instanceof OptArgNode)
{
visitOptArgNode((OptArgNode) child);
}
}
}
}
private boolean isVisitable(Node node)
{
return !(node instanceof ArgumentNode) && node.getClass() != ListNode.class;
}
/**
* @param positionHolder
* @return
*/
protected IFormatterTextNode createTextNode(IFormatterDocument document, ISourcePositionHolder positionHolder)
{
return createTextNode(document, positionHolder.getPosition());
}
/**
* Locate the word 'end' by searching for it from right to left in the given document. The assumption here is that
* the 'end' word is actually there, and the only thing that separate us from reaching it are white-spaces.
*
* @param document
* @param rightOffset
* @return The start offset of the 'end' word
*/
protected int locateEndOffset(IFormatterDocument document, int rightOffset)
{
String toLocate = "end"; //$NON-NLS-1$
int wordLength = toLocate.length();
do
{
int leftOffset = rightOffset - wordLength;
String endString = document.get(leftOffset, rightOffset);
if (toLocate.equals(endString))
{
// found it!
return leftOffset;
}
rightOffset--;
}
while (rightOffset - wordLength >= 0);
// if we got here, we didn't find the 'end'
return rightOffset;
}
/**
* Look for the given char in the document and return it's position.
*
* @param document
* @param offset
* @param c
* @return The char offset; -1 if no matching char was found
*/
protected int charLookup(final IFormatterDocument document, int offset, char c)
{
while (offset + 1 < document.getLength())
{
if (document.charAt(offset) == c)
{
return offset;
}
offset++;
}
return -1;
}
/**
* @param position
* @return
*/
private IFormatterTextNode createTextNode(IFormatterDocument document, SourcePosition position)
{
return AbstractFormatterNodeBuilder
.createTextNode(document, position.getStartOffset(), position.getEndOffset());
}
/**
* Push a FormatterStringNode for the given position.
*
* @param position
*/
private void pushTextNode(SourcePosition position)
{
int startOffset = position.getStartOffset();
int endOffset = position.getEndOffset();
if (startOffset < endOffset)
{
pushTextNode(startOffset, endOffset);
}
}
/**
* Push a FormatterStringNode for the given start and end offsets.
*
* @param startOffset
* @param endOffset
*/
private void pushTextNode(int startOffset, int endOffset)
{
FormatterStringNode strNode = new FormatterStringNode(document, startOffset, endOffset);
builder.addChild(strNode);
}
/**
* Returns true if the given position start and end lines are equal, or if the end-line was pushed only because
* new-lines and white spaces appear in the code.
*
* @param position
* @return
*/
private boolean isInSameLineExcludingWhitespaces(SourcePosition position)
{
if (position.getStartLine() == position.getEndLine())
{
return true;
}
// We split on new-lines, and in case the split result is giving us a String array of one element, we know that
// all the rest of the lines in that text were new-lines terminators.
String text = document.get(position.getStartOffset(), position.getEndOffset());
String[] linesSplit = LINE_SPLIT_PATTERN.split(text);
if (linesSplit.length == 1)
{
return true;
}
else if (linesSplit.length > 1)
{
int start = (linesSplit[0].trim().length() == 0) ? 0 : 1;
int end = (linesSplit[linesSplit.length - 1].trim().length() == 0) ? linesSplit.length
: linesSplit.length - 1;
boolean allWhiteSpace = true;
for (int i = start; i < end && allWhiteSpace; i++)
{
allWhiteSpace &= linesSplit[i].trim().length() == 0;
}
return allWhiteSpace;
}
return false;
}
protected static final Comparator<Node> POSITION_COMPARATOR = new Comparator<Node>()
{
public int compare(Node n1, Node n2)
{
return n1.getPosition().getStartOffset() - n2.getPosition().getStartOffset();
}
};
}