001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * If you wish your version of this file to be governed by only the CDDL
025: * or only the GPL Version 2, indicate your decision by adding
026: * "[Contributor] elects to include this software in this distribution
027: * under the [CDDL or GPL Version 2] license." If you do not indicate a
028: * single choice of license, a recipient has the option to distribute
029: * your version of this file under either the CDDL, the GPL Version 2 or
030: * to extend the choice of license to its licensees as provided above.
031: * However, if you add GPL Version 2 code and therefore, elected the GPL
032: * Version 2 license, then the option applies only if the new code is
033: * made subject to such option by the copyright holder.
034: *
035: * Contributor(s):
036: *
037: * Portions Copyrighted 2007 Sun Microsystems, Inc.
038: */
039: package org.netbeans.modules.ruby.hints.introduce;
040:
041: import org.netbeans.modules.ruby.ParseTreeWalker;
042: import java.io.IOException;
043: import java.util.ArrayList;
044: import java.util.Collections;
045: import java.util.HashMap;
046: import java.util.List;
047: import java.util.Map;
048: import javax.swing.text.BadLocationException;
049: import javax.swing.text.JTextComponent;
050: import org.jruby.ast.ClassNode;
051: import org.jruby.ast.Node;
052: import org.jruby.ast.NodeTypes;
053: import org.jruby.lexer.yacc.ISourcePosition;
054: import org.netbeans.modules.gsf.api.CompilationInfo;
055: import org.netbeans.modules.gsf.api.OffsetRange;
056: import org.netbeans.editor.BaseDocument;
057: import org.netbeans.editor.Utilities;
058: import org.netbeans.modules.ruby.AstUtilities;
059: import org.netbeans.modules.ruby.Formatter;
060: import org.netbeans.modules.ruby.NbUtilities;
061: import org.netbeans.modules.ruby.RubyUtils;
062: import org.netbeans.modules.ruby.hints.spi.Description;
063: import org.netbeans.modules.ruby.hints.spi.Fix;
064: import org.netbeans.modules.ruby.hints.spi.HintSeverity;
065: import org.netbeans.modules.ruby.hints.spi.RuleContext;
066: import org.netbeans.modules.ruby.hints.spi.SelectionRule;
067: import org.openide.util.Exceptions;
068: import org.openide.util.NbBundle;
069:
070: /**
071: * Offer to introduce a variable for an expression
072: *
073: * @todo If you just select an identifier I shouldn't offer to abstract it - how could it
074: * possibly help?
075: * @todo Suggest name: leaf if attribute or method access
076: * @todo If you select the RHS of an assignment, don't offer to introduce a constant, field or
077: * variable - it's already assigned!
078: * @todo Support replace all duplicates
079: * @todo Test hashes
080: * @todo If you have comments at the beginning or end of the selection, I don't handle things right -
081: * I end up with the wrong AST offsets (and I can't just skip these; they need to be included in the
082: * move!)
083: * @todo For statements containing break/next/continue I just disable this refactoring now; fix this
084: * such that I can handle these statements by looking up the loop construct and allowing it if
085: * it's all within the fragment!
086: * @todo If I extract method, and there is only a single return value, and the last statement
087: * in the method assigns to that return value, there's no point in having an explicit return
088: * of it, just leave the statement as the last statement
089: * @todo Invoke formatter via the infrastructure so that it works right in RHTML etc.
090: *
091: * @author Tor Norbye
092: */
093: public class IntroduceHint implements SelectionRule {
094: /** For test infrastructure only - a way to bypass the interactive name dialog */
095: static String testName;
096:
097: public void run(RuleContext context, List<Description> result) {
098: CompilationInfo info = context.compilationInfo;
099: int start = context.selectionStart;
100: int end = context.selectionEnd;
101:
102: assert start < end;
103:
104: try {
105: BaseDocument doc = (BaseDocument) info.getDocument();
106: if (end > doc.getLength()) {
107: return;
108: }
109:
110: if (end - start > 1000) {
111: // Avoid doing tons of work when the user does a Ctrl-A to select all in a really
112: // large buffer.
113: return;
114: }
115:
116: if (Formatter.getTokenBalance(doc, start, end, true,
117: RubyUtils.isRhtmlDocument(doc)) != 0) {
118: return;
119: }
120:
121: Node root = AstUtilities.getRoot(info);
122: if (root == null) {
123: return;
124: }
125: OffsetRange lexOffsets = adjustOffsets(info, doc, start,
126: end);
127: if (lexOffsets == OffsetRange.NONE) {
128: return;
129: }
130:
131: OffsetRange astOffsets = AstUtilities.getAstOffsets(info,
132: lexOffsets);
133: if (astOffsets == OffsetRange.NONE) {
134: return;
135: }
136:
137: int astStart = astOffsets.getStart();
138: int astEnd = astOffsets.getEnd();
139: Map<Integer, List<Node>> nodeDepthMap = new HashMap<Integer, List<Node>>();
140: findApplicableNodes(root, astStart, astEnd, nodeDepthMap, 0);
141: if (nodeDepthMap.keySet().size() != 1) {
142: // Either nodes at multiple depths or no nodes at all
143: return;
144: }
145: List<Node> nodes = nodeDepthMap.values().iterator().next();
146: assert nodes.size() > 0;
147:
148: IntroduceKindFinder typeChecker = new IntroduceKindFinder();
149: ParseTreeWalker walker = new ParseTreeWalker(typeChecker);
150: for (Node node : nodes) {
151: walker.walk(node);
152: }
153: List<IntroduceKind> kinds = typeChecker.getKinds();
154:
155: if (kinds == null || kinds.size() == 0) {
156: return;
157: }
158:
159: OffsetRange range = new OffsetRange(start, end);
160:
161: // Adjust the fix range to be right around the dot so that the light bulb ends up
162: // on the same line as the caret and alt-enter works
163: JTextComponent target = NbUtilities.getPaneFor(info
164: .getFileObject());
165: if (target != null) {
166: int dot = target.getCaret().getDot();
167: if (start == dot) {
168: range = new OffsetRange(start, start);
169: } else if (end == dot) {
170: range = new OffsetRange(end, end);
171: }
172: }
173:
174: if (RubyUtils.isRhtmlDocument(doc)) {
175: // In RHTML, only Introduce Variable is permitted
176: kinds.retainAll(Collections
177: .singleton(IntroduceKind.CREATE_VARIABLE));
178: } else if (kinds.contains(IntroduceKind.CREATE_FIELD)) {
179: // Also create a field? Only if we're inside a class
180: ClassNode clz = AstUtilities.findClassAtOffset(root,
181: start);
182: if (clz == null) {
183: kinds.remove(IntroduceKind.CREATE_FIELD);
184: if (kinds.size() == 0) {
185: return;
186: }
187: }
188: }
189:
190: for (IntroduceKind kind : kinds) {
191: IntroduceFix fix = new IntroduceFix(info, nodes,
192: lexOffsets, astOffsets, kind);
193: List<Fix> fixList = new ArrayList<Fix>(1);
194: fixList.add(fix);
195: String displayName = fix.getDescription();
196: Description desc = new Description(this , displayName,
197: info.getFileObject(), range, fixList, 292);
198: result.add(desc);
199: }
200: } catch (BadLocationException ex) {
201: Exceptions.printStackTrace(ex);
202: } catch (IOException ex) {
203: Exceptions.printStackTrace(ex);
204: }
205: }
206:
207: public boolean appliesTo(CompilationInfo info) {
208: return true;
209: }
210:
211: public String getDisplayName() {
212: return NbBundle
213: .getMessage(IntroduceHint.class, "IntroduceHint");
214: }
215:
216: // Only used by configurable rules
217: //public String getDescription() {
218: // return NbBundle.getMessage(IntroduceHint.class, "IntroduceHintDesc");
219: //}
220:
221: public boolean showInTasklist() {
222: return false;
223: }
224:
225: public HintSeverity getDefaultSeverity() {
226: return HintSeverity.CURRENT_LINE_WARNING;
227: }
228:
229: private OffsetRange adjustOffsets(CompilationInfo info,
230: BaseDocument doc, int start, int end)
231: throws BadLocationException {
232: int startRowEnd = Utilities.getRowLastNonWhite(doc, start);
233: if (startRowEnd == -1) {
234: startRowEnd = Utilities.getRowEnd(doc, end);
235: } else {
236: startRowEnd += 1; // Points at beginning of last char rather than after it, so adjust
237: }
238: int adjustedStart;
239: if (start >= startRowEnd) {
240: // Go to the next line
241: adjustedStart = Utilities.getRowEnd(doc, start) + 1;
242: if (adjustedStart <= doc.getLength()) {
243: int nextRow = Utilities.getRowFirstNonWhite(doc,
244: adjustedStart);
245: if (nextRow != -1) {
246: adjustedStart = nextRow;
247: }
248: } else {
249: adjustedStart = doc.getLength();
250: }
251: } else {
252: adjustedStart = Math.max(start, Utilities
253: .getRowFirstNonWhite(doc, start));
254: }
255:
256: int rowBegin = Utilities.getRowFirstNonWhite(doc, end);
257: int adjustedEnd;
258: // Go to the previous row if you're on a blank line or the beginning of a line
259: if (rowBegin == -1) {
260: adjustedEnd = Math.max(0,
261: Utilities.getRowStart(doc, end) - 1);
262: } else {
263: if (end <= rowBegin) {
264: adjustedEnd = Math.max(0, Utilities.getRowStart(doc,
265: end) - 1);
266: } else {
267: int rowEnd = Utilities.getRowLastNonWhite(doc, end);
268: adjustedEnd = Math.min(end, rowEnd + 1);
269: }
270: }
271:
272: adjustedStart = Math.min(adjustedStart, doc.getLength());
273: adjustedEnd = Math.min(adjustedEnd, doc.getLength());
274:
275: if (adjustedEnd <= adjustedStart) {
276: return OffsetRange.NONE;
277: }
278:
279: return new OffsetRange(adjustedStart, adjustedEnd);
280: }
281:
282: /** Compute the set of applicable AST nodes for the given selection.
283: * It will find a set of continguous nodes in the AST. The result is returned in the
284: * result parameter. No nodes are added if the selection does not correspond to a complete
285: * expression or set of statements.
286: * @return The depth of the matches, or NODESEARCH_INCONSISTENT if the result set
287: * is invalid, or NODESEARCH_NOT_FOUND if no matches were found.
288: */
289: private void findApplicableNodes(Node node, int start, int end,
290: Map<Integer, List<Node>> result, int depth) {
291: @SuppressWarnings(value="unchecked")
292: List<Node> list = node.childNodes();
293:
294: for (Node child : list) {
295: if (child.nodeId == NodeTypes.NEWLINENODE
296: || child.nodeId == NodeTypes.HASHNODE) {
297: // Newlines and hasnodes have incorrect offsets, so always search their children
298: // instead of applying below search pruning logic
299: findApplicableNodes(child, start, end, result,
300: depth + 1);
301: } else {
302: boolean add = false;
303: ISourcePosition pos = child.getPosition();
304: if (pos.getStartOffset() >= start
305: && pos.getEndOffset() <= end) {
306: add = true;
307: } else
308: // Prune search only to nodes that can possibly contain the children
309: if (pos.getStartOffset() <= start
310: && pos.getEndOffset() >= end) {
311: if (pos.getStartOffset() == start
312: && pos.getEndOffset() == end) {
313: add = true;
314: } else {
315: findApplicableNodes(child, start, end, result,
316: depth + 1);
317: }
318: } else {
319: // Partial overlap
320: if (pos.getStartOffset() <= start
321: && start <= pos.getEndOffset()) {
322: findApplicableNodes(child, start, end, result,
323: depth + 1);
324: } else if (pos.getStartOffset() <= end
325: && end <= pos.getEndOffset()) {
326: findApplicableNodes(child, start, end, result,
327: depth + 1);
328: }
329: }
330: if (add) {
331: List<Node> l = result.get(depth);
332: if (l == null) {
333: l = new ArrayList<Node>();
334: result.put(depth, l);
335: }
336: l.add(child);
337: }
338: }
339: }
340: }
341: }
|