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;
040:
041: import java.io.IOException;
042: import java.util.Collections;
043: import java.util.List;
044: import java.util.Set;
045: import java.util.prefs.Preferences;
046: import javax.swing.JComponent;
047: import javax.swing.text.BadLocationException;
048: import org.jruby.ast.IfNode;
049: import org.jruby.ast.Node;
050: import org.jruby.ast.NodeTypes;
051: import org.netbeans.modules.gsf.api.CompilationInfo;
052: import org.netbeans.modules.gsf.api.OffsetRange;
053: import org.netbeans.api.lexer.TokenHierarchy;
054: import org.netbeans.api.lexer.TokenSequence;
055: import org.netbeans.editor.BaseDocument;
056: import org.netbeans.modules.ruby.AstUtilities;
057: import org.netbeans.modules.ruby.RubyUtils;
058: import org.netbeans.modules.ruby.hints.spi.AstRule;
059: import org.netbeans.modules.ruby.hints.spi.Description;
060: import org.netbeans.modules.ruby.hints.spi.EditList;
061: import org.netbeans.modules.ruby.hints.spi.Fix;
062: import org.netbeans.modules.ruby.hints.spi.HintSeverity;
063: import org.netbeans.modules.ruby.hints.spi.PreviewableFix;
064: import org.netbeans.modules.ruby.hints.spi.RuleContext;
065: import org.netbeans.modules.ruby.lexer.LexUtilities;
066: import org.openide.util.Exceptions;
067: import org.openide.util.NbBundle;
068:
069: /**
070: * Convert conditionals of the form "if foo; bar; end" to "bar if foo".
071: * Inspired by the excellent blog entry
072: * http://langexplr.blogspot.com/2007/11/creating-netbeans-ruby-hints-with-scala_24.html
073: * by Luis Diego Fallas.
074: *
075: * @author Tor Norbye
076: */
077: public class ConvertConditionals implements AstRule {
078:
079: public Set<Integer> getKinds() {
080: return Collections.singleton(NodeTypes.IFNODE);
081: }
082:
083: public void run(RuleContext context, List<Description> result) {
084: Node node = context.node;
085: CompilationInfo info = context.compilationInfo;
086:
087: IfNode ifNode = (IfNode) node;
088: if (ifNode.getCondition() == null) {
089: // Can happen for this code:
090: // if ()
091: // end
092: // (typically while editing)
093: return;
094: }
095: Node body = ifNode.getThenBody();
096: Node elseNode = ifNode.getElseBody();
097:
098: if (body != null && elseNode != null) {
099: // Can't convert if-then-else conditionals
100: return;
101: }
102:
103: if (body == null && elseNode == null) {
104: // Can't convert empty conditions
105: return;
106: }
107:
108: // Can't convert if !x/elseif blocks
109: if (ifNode.getElseBody() != null
110: && ifNode.getElseBody().nodeId == NodeTypes.IFNODE) {
111: return;
112: }
113:
114: int start = ifNode.getPosition().getStartOffset();
115: if (body != null && (
116: // Can't convert blocks with multiple statements
117: body.nodeId == NodeTypes.BLOCKNODE ||
118: // Already a statement modifier?
119: body.getPosition().getStartOffset() <= start)) {
120: return;
121: } else if (elseNode != null
122: && (elseNode.nodeId == NodeTypes.BLOCKNODE || elseNode
123: .getPosition().getStartOffset() <= start)) {
124: return;
125: }
126:
127: try {
128: BaseDocument doc = (BaseDocument) info.getDocument();
129: int keywordOffset = ConvertIfToUnless.findKeywordOffset(
130: info, ifNode);
131: if (keywordOffset == -1
132: || keywordOffset > doc.getLength() - 1) {
133: return;
134: }
135:
136: char k = doc.getText(keywordOffset, 1).charAt(0);
137: if (!(k == 'i' || k == 'u')) {
138: return; // Probably ternary operator, ?:
139: }
140: } catch (BadLocationException ble) {
141: Exceptions.printStackTrace(ble);
142: } catch (IOException ioe) {
143: Exceptions.printStackTrace(ioe);
144: }
145:
146: // If statement that is not already a statement modifier
147: OffsetRange range = AstUtilities.getRange(node);
148:
149: if (RubyUtils.isRhtmlFile(info.getFileObject())) {
150: // Make sure that we're in a single contiguous Ruby section; if not, this won't work
151: range = LexUtilities.getLexerOffsets(info, range);
152: if (range == OffsetRange.NONE) {
153: return;
154: }
155:
156: BaseDocument doc = null;
157: try {
158: doc = (BaseDocument) info.getDocument();
159: doc.readLock();
160: TokenHierarchy th = TokenHierarchy.get(doc);
161: TokenSequence ts = th.tokenSequence();
162: ts.move(range.getStart());
163: if (!ts.moveNext() && !ts.movePrevious()) {
164: return;
165: }
166:
167: if (ts.offset() + ts.token().length() < range.getEnd()) {
168: return;
169: }
170: } catch (IOException ex) {
171: Exceptions.printStackTrace(ex);
172: } finally {
173: if (doc != null) {
174: doc.readUnlock();
175: }
176: }
177: }
178:
179: ConvertToModifier fix = new ConvertToModifier(info, ifNode);
180:
181: if (fix.getEditList() == null) {
182: return;
183: }
184:
185: List<Fix> fixes = Collections.<Fix> singletonList(fix);
186:
187: String displayName = NbBundle.getMessage(
188: ConvertConditionals.class, "ConvertConditionals");
189: Description desc = new Description(this , displayName, info
190: .getFileObject(), range, fixes, 500);
191: result.add(desc);
192: }
193:
194: public String getId() {
195: return "ConvertConditionals"; // NOI18N
196: }
197:
198: public String getDescription() {
199: return NbBundle.getMessage(ConvertConditionals.class,
200: "ConvertConditionalsDesc");
201: }
202:
203: public boolean getDefaultEnabled() {
204: return true;
205: }
206:
207: public JComponent getCustomizer(Preferences node) {
208: return null;
209: }
210:
211: public boolean appliesTo(CompilationInfo info) {
212: return true;
213: }
214:
215: public String getDisplayName() {
216: return NbBundle.getMessage(ConvertConditionals.class,
217: "ConvertConditionals");
218: }
219:
220: public boolean showInTasklist() {
221: return false;
222: }
223:
224: public HintSeverity getDefaultSeverity() {
225: return HintSeverity.CURRENT_LINE_WARNING;
226: }
227:
228: private class ConvertToModifier implements PreviewableFix {
229: private CompilationInfo info;
230: private IfNode ifNode;
231:
232: public ConvertToModifier(CompilationInfo info, IfNode ifNode) {
233: this .info = info;
234: this .ifNode = ifNode;
235: }
236:
237: public String getDescription() {
238: return NbBundle.getMessage(ConvertConditionals.class,
239: "ConvertConditionalsFix");
240: }
241:
242: public void implement() throws Exception {
243: EditList edits = getEditList();
244: if (edits != null) {
245: edits.apply();
246: }
247: }
248:
249: public EditList getEditList() {
250: try {
251: BaseDocument doc = (BaseDocument) info.getDocument();
252:
253: Node bodyNode = ifNode.getThenBody();
254: boolean isIf = bodyNode != null;
255: if (bodyNode == null) {
256: bodyNode = ifNode.getElseBody();
257: }
258: OffsetRange bodyRange = AstUtilities.getRange(bodyNode);
259: bodyRange = LexUtilities.getLexerOffsets(info,
260: bodyRange);
261: if (bodyRange == OffsetRange.NONE) {
262: return null;
263: }
264:
265: String body = doc.getText(bodyRange.getStart(),
266: bodyRange.getLength()).trim();
267: if (body.endsWith(";")) {
268: body = body.substring(0, body.length() - 1);
269: }
270: StringBuilder sb = new StringBuilder();
271: sb.append(body);
272: sb.append(" ");
273: sb.append(isIf ? "if" : "unless"); // NOI18N
274: sb.append(" ");
275: OffsetRange range = AstUtilities.getRange(ifNode
276: .getCondition());
277: range = LexUtilities.getLexerOffsets(info, range);
278: if (range == OffsetRange.NONE) {
279: return null;
280: }
281: sb.append(doc.getText(range.getStart(), range
282: .getLength()));
283:
284: OffsetRange ifRange = AstUtilities.getRange(ifNode);
285: ifRange = LexUtilities.getLexerOffsets(info, ifRange);
286: if (ifRange == OffsetRange.NONE) {
287: return null;
288: }
289:
290: return new EditList(doc).replace(ifRange.getStart(),
291: ifRange.getLength(), sb.toString(), false, 0);
292: } catch (Exception ex) {
293: Exceptions.printStackTrace(ex);
294: return null;
295: }
296: }
297:
298: public boolean isSafe() {
299: return true;
300: }
301:
302: public boolean isInteractive() {
303: return false;
304: }
305:
306: public boolean canPreview() {
307: return true;
308: }
309: }
310: }
|