/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
*
* Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common
* Development and Distribution License("CDDL") (collectively, the
* "License"). You may not use this file except in compliance with the
* License. You can obtain a copy of the License at
* http://www.netbeans.org/cddl-gplv2.html
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
* specific language governing permissions and limitations under the
* License. When distributing the software, include this License Header
* Notice in each file and include the License file at
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the
* License Header, with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* If you wish your version of this file to be governed by only the CDDL
* or only the GPL Version 2, indicate your decision by adding
* "[Contributor] elects to include this software in this distribution
* under the [CDDL or GPL Version 2] license." If you do not indicate a
* single choice of license, a recipient has the option to distribute
* your version of this file under either the CDDL, the GPL Version 2 or
* to extend the choice of license to its licensees as provided above.
* However, if you add GPL Version 2 code and therefore, elected the GPL
* Version 2 license, then the option applies only if the new code is
* made subject to such option by the copyright holder.
*
* Contributor(s):
*
* Portions Copyrighted 2007 Sun Microsystems, Inc.
*/
package org.netbeans.modules.ruby.hints.introduce;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.MissingResourceException;
import java.util.Set;
import javax.swing.JButton;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import javax.swing.text.Position;
import org.jrubyparser.ast.MethodDefNode;
import org.jrubyparser.ast.Node;
import org.jrubyparser.ast.NodeType;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.Utilities;
import org.netbeans.modules.csl.api.EditList;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.csl.api.PreviewableFix;
import org.netbeans.modules.csl.spi.GsfUtilities;
import org.netbeans.modules.csl.spi.ParserResult;
import org.netbeans.modules.ruby.AstPath;
import org.netbeans.modules.ruby.AstUtilities;
import org.netbeans.modules.ruby.ParseTreeWalker;
import org.netbeans.modules.ruby.RubyIndex;
import org.netbeans.modules.ruby.RubyUtils;
import org.netbeans.modules.ruby.hints.infrastructure.RubyRuleContext;
import org.netbeans.modules.ruby.lexer.LexUtilities;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
/**
* The actual fix-implementation for introducing methods, fields, etc.
*
* @author Tor Norbye
*/
class IntroduceFix implements PreviewableFix {
/** Keep in sync with copy in CodeCompleter */
private static final boolean FORCE_COMPLETION_SPACES = Boolean.getBoolean("ruby.complete.spaces"); // NOI18N
private static final boolean COMMENT_NEW_ELEMENTS = !Boolean.getBoolean("ruby.create.nocomments"); // NOI18N
private final RubyRuleContext context;
private final ParserResult info;
private final OffsetRange lexRange;
private final OffsetRange astRange;
private final IntroduceKind kind;
private final List<Node> nodes;
private final BaseDocument doc;
private int commentOffset = -1;
IntroduceFix(RubyRuleContext context, List<Node> nodes, OffsetRange lexRange, OffsetRange astRange, IntroduceKind kind) {
this.context = context;
this.nodes = nodes;
this.lexRange = lexRange;
this.astRange = astRange;
this.kind = kind;
this.info = context.parserResult;
this.doc = context.doc;
}
public String getKeyExt() {
switch (kind) {
case CREATE_CONSTANT:
return "IntroduceConstant"; //NOI18N
case CREATE_VARIABLE:
return "IntroduceVariable"; //NOI18N
case CREATE_METHOD:
return "IntroduceMethod"; //NOI18N
case CREATE_FIELD:
return "IntroduceField"; //NOI18N
default:
throw new IllegalStateException(kind.toString());
}
}
public String getDescription() {
return NbBundle.getMessage(IntroduceHint.class, "FIX_" + getKeyExt()); //NOI18N
}
public void implement() throws Exception {
String name = null;
EditList edits = createEdits(name);
if (edits == null) {
// Some kind of error
return;
}
Position commentPosition = edits.createPosition(commentOffset);
edits.apply();
// Warp to the inserted method and show the comment
if (commentPosition != null && commentPosition.getOffset() != -1) {
JTextComponent target = GsfUtilities.getPaneFor(RubyUtils.getFileObject(info));
if (target != null) {
int offset = commentPosition.getOffset();
String commentText = getCommentText();
if (offset+commentText.length() <= doc.getLength()) {
String s = doc.getText(offset, commentText.length());
if (commentText.equals(s)) {
target.select(offset, offset+commentText.length());
}
}
}
}
}
private String getCommentText() throws MissingResourceException {
return NbBundle.getMessage(IntroduceHint.class, "DefaultMethodComment");
}
public EditList getEditList() {
String name = "new_name";
try {
return createEdits(name);
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
return null;
}
}
private EditList createEdits(String name) throws Exception {
String guessedName = AstUtilities.guessName(info, lexRange, astRange);
RubyIndex index = RubyIndex.get(info);
AstPath startPath = new AstPath(AstUtilities.getRoot(info), astRange.getStart());
List<OffsetRange> duplicates = null;
if (name == null) {
switch (kind) {
case CREATE_VARIABLE:
case CREATE_CONSTANT: {
Node startNode = nodes.get(0);
Node endNode = nodes.get(nodes.size()-1);
Node top = AstUtilities.getRoot(info);
int numDuplicates = 1;
if (kind == IntroduceKind.CREATE_CONSTANT) {
Node cls = AstUtilities.findClass(startPath);
if (cls != null) {
top = cls;
}
duplicates = DuplicateDetector.findDuplicates(info, doc, top, nodes, startNode, endNode);
numDuplicates = duplicates == null ? 1 : duplicates.size();
}
JButton btnOk = new JButton(NbBundle.getMessage(IntroduceHint.class, "LBL_Ok"));
JButton btnCancel = new JButton(NbBundle.getMessage(IntroduceHint.class, "LBL_Cancel"));
Set<String> takenNames;
if (kind == IntroduceKind.CREATE_CONSTANT) {
takenNames = AstUtilities.getUsedConstants(index, startPath);
} else {
takenNames = AstUtilities.getUsedLocalNames(startPath, startPath.leaf());
}
IntroduceVariablePanel panel = new IntroduceVariablePanel(numDuplicates, guessedName,
kind == IntroduceKind.CREATE_CONSTANT, btnOk, takenNames);
String caption = NbBundle.getMessage(IntroduceHint.class, "CAP_" + getKeyExt()); //NOI18N
DialogDescriptor dd = new DialogDescriptor(panel, caption, true,
new Object[]{btnOk, btnCancel}, btnOk, DialogDescriptor.DEFAULT_ALIGN, null,
null);
if (DialogDisplayer.getDefault().notify(dd) != btnOk) {
return null;//cancel
}
name = panel.getVariableName();
if (!panel.isReplaceAll()) {
duplicates = Collections.emptyList();
}
break;
}
case CREATE_FIELD: {
int numDuplicates = 1;
JButton btnOk = new JButton(NbBundle.getMessage(IntroduceHint.class, "LBL_Ok"));
JButton btnCancel = new JButton(NbBundle.getMessage(IntroduceHint.class, "LBL_Cancel"));
// TODO Allow choice between inserting in constructor, in method, etc.
int[] initilizeIn = new int[1];
Set<String> takenNames = AstUtilities.getUsedFields(index, startPath);
IntroduceFieldPanel panel = new IntroduceFieldPanel(guessedName, initilizeIn, numDuplicates, btnOk, takenNames);
String caption = NbBundle.getMessage(IntroduceHint.class, "CAP_" + getKeyExt()); //NOI18N
DialogDescriptor dd = new DialogDescriptor(panel, caption, true,
new Object[]{btnOk, btnCancel}, btnOk, DialogDescriptor.DEFAULT_ALIGN, null,
null);
if (DialogDisplayer.getDefault().notify(dd) != btnOk) {
return null;//cancel
}
name = panel.getFieldName();
if (!panel.isReplaceAll()) {
duplicates = Collections.emptyList();
}
break;
}
case CREATE_METHOD: {
JButton btnOk = new JButton(NbBundle.getMessage(IntroduceHint.class, "LBL_Ok"));
JButton btnCancel = new JButton(NbBundle.getMessage(IntroduceHint.class, "LBL_Cancel"));
Set<String> takenNames = AstUtilities.getUsedMethods(index, startPath);
IntroduceMethodPanel panel = new IntroduceMethodPanel("", takenNames); //NOI18N
panel.setOkButton( btnOk );
String caption = NbBundle.getMessage(IntroduceHint.class, "CAP_IntroduceMethod");
DialogDescriptor dd = new DialogDescriptor(panel, caption, true,
new Object[]{btnOk, btnCancel}, btnOk, DialogDescriptor.DEFAULT_ALIGN, null,
null);
if (DialogDisplayer.getDefault().notify(dd) != btnOk) {
return null;//cancel
}
name = panel.getMethodName();
break;
}
}
}
if (kind == IntroduceKind.CREATE_FIELD) {
name = "@" + name;
} else if (kind == IntroduceKind.CREATE_CONSTANT) {
//name = RubyUtils.underlinedNameToCamel(name);
name = name.toUpperCase();
}
if (kind == IntroduceKind.CREATE_CONSTANT || kind == IntroduceKind.CREATE_VARIABLE || kind == IntroduceKind.CREATE_FIELD) {
return introduceExp(name, duplicates);
} else {
assert kind == IntroduceKind.CREATE_METHOD;
// XXX TODO
return extractMethod(name);
}
}
private EditList introduceExp(String name, List<OffsetRange> duplicates) throws BadLocationException {
boolean isConstant = kind == IntroduceKind.CREATE_CONSTANT;
int begin;
if (isConstant) {
begin = findClassBegin();
if (begin == -1) {
begin = findMethodBegin();
}
if (begin == -1) {
// Not in a method - just place it before the method
begin = findStatementBegin();
} else {
// Jump to the beginning of the line
begin = Utilities.getRowStart(doc, begin);
}
} else {
begin = findStatementBegin();
}
int lexStart = lexRange.getStart();
int lexEnd = lexRange.getEnd();
assert begin <= lexStart;
StringBuilder sb = new StringBuilder();
if (isConstant && COMMENT_NEW_ELEMENTS) {
// TODO - insert a code template for editing the comment?
sb.append("# ");
}
int commentTextDelta = sb.length();
if (isConstant && COMMENT_NEW_ELEMENTS) {
sb.append(getCommentText());
sb.append("\n");
}
sb.append(name);
sb.append(" = ");
AstPath path = new AstPath(AstUtilities.getRoot(info), astRange.getStart());
boolean addHash = false;
if (path.leafGrandParent() != null && path.leafGrandParent().getNodeType() == NodeType.HASHNODE) {
addHash = true;
}
if (addHash) {
sb.append("{ ");
}
sb.append(doc.getText(lexStart, lexEnd - lexStart));
if (addHash) {
sb.append(" }");
}
sb.append("\n");
if (isConstant) {
sb.append("\n");
}
commentOffset = -1;
if (isConstant && begin > 0 && COMMENT_NEW_ELEMENTS) {
commentOffset = begin+commentTextDelta;
}
EditList edits = new EditList(doc);
edits.setFormatAll(false);
edits.replace(lexStart, lexEnd-lexStart, name, true, 1);
edits.replace(begin, 0, sb.toString(), true, 2);
if (duplicates != null && duplicates.size() > 1) {
Set<Integer> starts = new HashSet<Integer>();
starts.add(lexStart);
starts.add(begin);
for (OffsetRange range : duplicates) {
int start = range.getStart();
if (!starts.contains(start)) {
edits.replace(start, range.getLength(), name, true, 0);
starts.add(start);
}
}
}
return edits;
}
private EditList extractMethod(String name) throws BadLocationException {
Node startNode = nodes.get(0);
Node endNode = nodes.get(nodes.size()-1);
//AstPath startPath = new AstPath(AstUtilities.getRoot(info), start);
//AstPath endPath = new AstPath(AstUtilities.getRoot(info), end);
//Node startNode = startPath.leaf();
//Node endNode = endPath.leaf();
// Somewhere in the middle to ensure we pick everything up
// XXX That ain't right either - it might filter out blocks within the code fragment!
AstPath startPath = new AstPath(AstUtilities.getRoot(info), astRange.getStart()+astRange.getLength()/2);
List<Node> applicableBlocks = AstUtilities.getApplicableBlocks(startPath, true);
InputOutputVarFinder varFinder = new InputOutputVarFinder(startNode, endNode, applicableBlocks);
ParseTreeWalker walker = new ParseTreeWalker(varFinder);
Node method = AstUtilities.findLocalScope(startPath.leaf(), startPath);
walker.walk(method);
Set<String> inputs = varFinder.getInputVars();
Set<String> outputs = varFinder.getOutputVars();
List<String> inputVars = new ArrayList<String>(inputs);
Collections.sort(inputVars);
List<String> outputVars = new ArrayList<String>(outputs);
Collections.sort(outputVars);
// TODO: Compute local and dynamic variables; pass these in to the method
// TODO: Compute side effects (assignments to local and dynamic variables);
// these should be "return values"
// TODO: Worry about exceptions and control flow (yields, nexts, continues,etc)
int prevEnd = findMethodEnd();
// TODO - validate that the name is unique etc. so we don't accidentally rewrite it!
StringBuilder sb = new StringBuilder();
EditList edits = new EditList(doc);
edits.setFormatAll(false);
boolean isAbove = prevEnd < astRange.getStart();
sb.append("\n");
if (!isAbove) {
sb.append("\n");
}
sb.append("# ");
int commentTextDelta = sb.length();
sb.append(getCommentText());
sb.append("\n");
sb.append("def ");
sb.append(name);
if (inputVars.size() > 0) {
if (FORCE_COMPLETION_SPACES) {
appendCommaList(sb, inputVars, " ", "");
} else {
appendCommaList(sb, inputVars, "(", ")");
}
}
sb.append('\n');
int lexStart = lexRange.getStart();
int lexEnd = lexRange.getEnd();
sb.append(doc.getText(lexStart, lexEnd-lexStart));
sb.append('\n');
if (outputVars.size() > 0) {
// Don't emit "return" unless we have multiple parameters
appendCommaList(sb, outputVars, outputVars.size() == 1 ? "" : "return ", "\n");
}
sb.append("end");
if (isAbove) {
sb.append("\n");
}
edits.replace(prevEnd, 0, sb.toString(), true, 0);
commentOffset = prevEnd+commentTextDelta;
sb = new StringBuilder();
if (outputVars.size() > 0) {
appendCommaList(sb, outputVars, null, null);
sb.append(" = ");
}
sb.append(name);
if (inputVars.size() > 0) {
if (FORCE_COMPLETION_SPACES) {
appendCommaList(sb, inputVars, " ", "");
} else {
appendCommaList(sb, inputVars, "(", ")");
}
}
edits.replace(lexStart, lexEnd-lexStart, sb.toString(), true, 0);
return edits;
}
private void appendCommaList(StringBuilder sb, List<String> items, String pre, String post) {
if (pre != null) {
sb.append(pre);
}
boolean first = true;
for (String item : items) {
if (first) {
first = false;
} else {
sb.append(", ");
}
sb.append(item);
}
if (post != null) {
sb.append(post);
}
}
private int findClassBegin() throws BadLocationException {
// Find the location of the beginning of the current statement
AstPath path = new AstPath(AstUtilities.getRoot(info), astRange.getStart());
Node cls = AstUtilities.findClass(path);
if (cls != null) {
int astPos = Utilities.getRowEnd(doc, cls.getPosition().getStartOffset())+1;
return Math.min(LexUtilities.getLexerOffset(info, astPos), doc.getLength());
}
return -1;
}
private int findStatementBegin() throws BadLocationException {
//if (RubyUtils.isRhtmlDocument(doc)) {
// // Special handling here?
//}
// Find the location of the beginning of the current statement
AstPath path = new AstPath(AstUtilities.getRoot(info), astRange.getStart());
Iterator<Node> it = path.leafToRoot();
Node prev = null;
boolean found = false;
while (it.hasNext()) {
Node n = it.next();
if (n.getNodeType() == NodeType.NEWLINENODE) {
if (prev != null) {
found = true;
// Peek ahead and see if we have another outer newline that is also on
// this line, e.g. if (x<y) will have a newline for the paren as well as
// for the actual line
Node p = n;
Node innerNewline = n;
while (it.hasNext()) {
n = it.next();
if (n.getNodeType() == NodeType.NEWLINENODE) {
int prevNewline = Math.min(LexUtilities.getLexerOffset(info, innerNewline.getPosition().getStartOffset()), doc.getLength());
int newLine = Math.min(LexUtilities.getLexerOffset(info, n.getPosition().getStartOffset()), doc.getLength());
if (p != null && newLine != -1 && prevNewline != -1 && Utilities.getRowStart(doc, prevNewline) == Utilities.getRowStart(doc, newLine)) {
prev = p;
}
break;
}
p = n;
}
}
break;
}
prev = n;
}
if (found) {
return Math.min(LexUtilities.getLexerOffset(info, prev.getPosition().getStartOffset()), doc.getLength());
} else {
// This is not right but a reasonable fallback
return Utilities.getRowFirstNonWhite(doc, lexRange.getStart());
}
}
/** Compute the end of the current method. If we're not in a method, compute a location
* inside the surrounding class. */
private int findMethodEnd() throws BadLocationException {
// TODO - I need an AST path for this!
AstPath path = new AstPath(AstUtilities.getRoot(info), astRange.getStart());
// Find the closest block node enclosing the given node
for (Node curr : path) {
if (curr.getNodeType() == NodeType.DEFNNODE || curr.getNodeType() == NodeType.DEFSNODE) {
return Math.min(LexUtilities.getLexerOffset(info, curr.getPosition().getEndOffset()), doc.getLength());
}
if (curr.getNodeType() == NodeType.CLASSNODE || curr.getNodeType() == NodeType.SCLASSNODE ||
curr.getNodeType() == NodeType.MODULENODE) {
// End of the class:
//int clzEnd = LexUtilities.getLexerOffset(info, curr.getPosition().getEndOffset());
//// Skip over "end"
//clzEnd -= 3;
//clzEnd = Math.min(clzEnd, doc.getLength());
//if (clzEnd == Utilities.getRowFirstNonWhite(doc, clzEnd)) {
// clzEnd = Utilities.getRowEnd(doc, Utilities.getRowStart(doc, clzEnd)-1);
//}
//return clzEnd;
// Beginning of the class:
int clzStart = LexUtilities.getLexerOffset(info, curr.getPosition().getStartOffset());
return Utilities.getRowEnd(doc, clzStart);
}
}
// Not inside a method - we're in top level scope so just
// use the end of the document
return doc.getLength();
}
/** Compute the beginning of the current method */
private int findMethodBegin() throws BadLocationException {
AstPath path = new AstPath(AstUtilities.getRoot(info), astRange.getStart());
MethodDefNode method = AstUtilities.findMethod(path);
if (method != null) {
int methodAstOffset = method.getPosition().getStartOffset();
int lexOffset = LexUtilities.getLexerOffset(info, methodAstOffset);
if (lexOffset != -1) {
OffsetRange comment = LexUtilities.findRDocRange(doc, methodAstOffset);
if (comment != OffsetRange.NONE) {
return comment.getStart();
}
}
return LexUtilities.getLexerOffset(info, methodAstOffset);
} else {
return -1;
}
}
public boolean isSafe() {
return true;
}
public boolean isInteractive() {
return true;
}
public boolean canPreview() {
return true;
}
}