/* * 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]" * * Contributor(s): * * Portions Copyrighted 2007 Sun Microsystems, Inc. */ package org.netbeans.modules.ruby.hints; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.prefs.Preferences; import javax.swing.JComponent; import javax.swing.text.BadLocationException; import org.jrubyparser.ast.Node; import org.jrubyparser.ast.NodeType; import org.jrubyparser.SourcePosition; import org.netbeans.api.lexer.Token; import org.netbeans.api.lexer.TokenId; import org.netbeans.api.lexer.TokenSequence; import org.netbeans.editor.BaseDocument; import org.netbeans.editor.Utilities; import org.netbeans.modules.csl.api.EditList; import org.netbeans.modules.csl.api.Hint; import org.netbeans.modules.csl.api.HintFix; import org.netbeans.modules.csl.api.HintSeverity; import org.netbeans.modules.csl.api.OffsetRange; import org.netbeans.modules.csl.api.PreviewableFix; import org.netbeans.modules.csl.api.RuleContext; import org.netbeans.modules.csl.spi.ParserResult; import org.netbeans.modules.ruby.AstPath; import org.netbeans.modules.ruby.AstUtilities; import org.netbeans.modules.ruby.RubyUtils; import org.netbeans.modules.ruby.hints.infrastructure.RubyAstRule; import org.netbeans.modules.ruby.hints.infrastructure.RubyRuleContext; import org.netbeans.modules.ruby.lexer.LexUtilities; import org.netbeans.modules.ruby.lexer.RubyTokenId; import org.openide.util.Exceptions; import org.openide.util.NbBundle; /** * Hint which adds a fix to lines containing a "single-line" definition * of a method or a class, and offers to expand it into a multi-line * definition, e.g. replacing * <pre> * def foo; bar; end * </pre> * with * <pre> * def foo * bar * end * </pre> * <p> * NOTE - this hint is only activated for the line under the caret! * * @todo Filter out the case where you have a def inside a class on the same line! * @todo Apply this tip to brace blocks as well - and offer both expand and collapse! * @todo Why doesn't this work on line begins? E.g. add "def foo; bar; end" and put the * caret to the left of "def"; it doesn't activate * @todo See James Moore's comment about formatting multi-line statements * * @author Tor Norbye */ public class ExpandSameLineDef extends RubyAstRule { public ExpandSameLineDef() { } public boolean appliesTo(RuleContext context) { ParserResult info = context.parserResult; // Skip for RHTML files for now - isn't implemented properly return RubyUtils.getFileObject(info).getMIMEType().equals("text/x-ruby"); } public Set<NodeType> getKinds() { Set<NodeType> types = new HashSet<NodeType>(); types.add(NodeType.CLASSNODE); types.add(NodeType.DEFNNODE); types.add(NodeType.DEFSNODE); return types; } public void run(RubyRuleContext context, List<Hint> result) { Node node = context.node; AstPath path = context.path; ParserResult info = context.parserResult; BaseDocument doc = context.doc; // Look for use of deprecated fields if (node.getNodeType() == NodeType.DEFNNODE || node.getNodeType() == NodeType.DEFSNODE || node.getNodeType() == NodeType.CLASSNODE) { SourcePosition pos = node.getPosition(); try { if (doc == null) { // Run on a file that was just closed return; } int start = pos.getStartOffset(); int end = pos.getEndOffset(); int length = doc.getLength(); if (Utilities.getRowEnd(doc, Math.min(start,length)) == Utilities.getRowEnd(doc, Math.min(end,length))) { // Block is on a single line // TODO - add a hint to turn off this hint? // Should be a utility or infrastructure option! Node root = AstUtilities.getRoot(info); if (path.leaf() != node) { path = new AstPath(root, node); } List<HintFix> fixList = Collections.<HintFix>singletonList(new ExpandLineFix(context, path)); OffsetRange range = new OffsetRange(pos.getStartOffset(), pos.getEndOffset()); Hint desc = new Hint(this, getDisplayName(), RubyUtils.getFileObject(info), range, fixList, 150); result.add(desc); // Exit; don't process children such that a def inside a class all // on the same line only produces a single suggestion for the outer block return; } } catch (BadLocationException ex){ Exceptions.printStackTrace(ex); } } } public void cancel() { // Does nothing } public String getId() { return "Expand_Same_Line_Def"; // NOI18N } public String getDisplayName() { return NbBundle.getMessage(ExpandSameLineDef.class, "ExpandLine"); } public String getDescription() { return NbBundle.getMessage(ExpandSameLineDef.class, "ExpandLineDesc"); } private static class ExpandLineFix implements PreviewableFix { private final RubyRuleContext context; private final AstPath path; ExpandLineFix(RubyRuleContext context, AstPath path) { this.context = context; this.path = path; } public String getDescription() { String code = path.leaf().getNodeType() == NodeType.DEFNNODE ? "def" : "class"; return NbBundle.getMessage(ExpandSameLineDef.class, "ExpandLineFix", code); } private void findLineBreaks(Node node, Set<Integer> offsets) { if (node.getNodeType() == NodeType.NEWLINENODE) { offsets.add(node.getPosition().getStartOffset()); } List<Node> list = node.childNodes(); for (Node child : list) { if (child.isInvisible()) { continue; } findLineBreaks(child, offsets); } } /** * Try to split a line like * class FooController; def rescue_action(e) raise e end; end * into multiple lines. We can use lexical tokens like ";" as a clue * to where to put newlines, but we want to use the AST too such that * we see that we need a newline between the argument (e) and raise in the * above line. * <p> * By using both we'll get some offsets in the same area so we'll need * to be careful when applying our ;-to-\n replacements and our \n insertions * so we don't get multiple newlines for places where both the AST and * the semicolons suggest we need newlines. */ public void implement() throws Exception { getEditList().apply(); } public EditList getEditList() throws Exception { BaseDocument doc = context.doc; SourcePosition pos = path.leaf().getPosition(); int startOffset = pos.getStartOffset(); int endOffset = pos.getEndOffset(); if (endOffset > doc.getLength()) { if (startOffset > doc.getLength()) { startOffset = doc.getLength(); } endOffset = doc.getLength(); } // Look through the document and find the statement separators (;); // at these locations I'll replace the ; with a newline and then // apply a formatter Set<Integer> offsetSet = new HashSet<Integer>(); findLineBreaks(path.leaf(), offsetSet); // Add in ; replacements TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, endOffset); if (ts != null) { // Traverse sequence in reverse order such that my offset list is in decreasing order ts.move(endOffset); while (ts.movePrevious() && ts.offset() > startOffset) { Token<?extends RubyTokenId> token = ts.token(); TokenId id = token.id(); if (id == RubyTokenId.IDENTIFIER && ";".equals(token.text().toString())) { // NOI18N offsetSet.add(ts.offset()); } else if (id == RubyTokenId.CLASS || id == RubyTokenId.DEF || id == RubyTokenId.END) { offsetSet.add(ts.offset()); } } } List<Integer> offsets = new ArrayList<Integer>(offsetSet); Collections.sort(offsets); // Ensure that we go in high to lower order such that I edit the // document from bottom to top (so offsets don't have to be adjusted // to account for our own edits along the way) Collections.reverse(offsets); EditList edits = new EditList(doc); if (offsets.size() > 0) { // TODO: Create a ModificationResult here and process it // The following is the WRONG way to do it... // I've gotta use a ModificationResult instead! List<Integer> newlines = new ArrayList<Integer>(); try { // Process offsets from back to front such that I can // modify the document without worrying that the other offsets // need to be adjusted int prev = -1; for (int offset : offsets) { // We might get some dupes since we add offsets from both // the AST newline nodes and semicolons discovered in the lexical token hierarchy if (offset == prev) { continue; } prev = offset; if (";".equals(doc.getText(offset, 1))) { // NOI18N edits.replace(offset, 1, null, false, 1); if (newlines.contains(offset+2)) { continue; } } if (newlines.contains(offset+1) || newlines.contains(offset)) { continue; } edits.replace(offset, 0, "\n", false, 2); // NOI18N newlines.add(offset); } edits.setFormatAll(true); } catch (BadLocationException ble) { Exceptions.printStackTrace(ble); } } return edits; } public boolean isSafe() { return true; } public boolean isInteractive() { return false; } public boolean canPreview() { return true; } } public boolean getDefaultEnabled() { return true; } public HintSeverity getDefaultSeverity() { return HintSeverity.CURRENT_LINE_WARNING; } public boolean showInTasklist() { return false; } public JComponent getCustomizer(Preferences node) { return null; } }