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: * Contributor(s):
025: *
026: * Portions Copyrighted 2007 Sun Microsystems, Inc.
027: */
028: package org.netbeans.modules.ruby.hints;
029:
030: import java.io.IOException;
031: import java.util.ArrayList;
032: import java.util.Collections;
033: import java.util.HashSet;
034: import java.util.List;
035: import java.util.Set;
036: import java.util.prefs.Preferences;
037: import javax.swing.JComponent;
038: import javax.swing.text.BadLocationException;
039: import org.jruby.ast.IArgumentNode;
040: import org.jruby.ast.Node;
041: import org.jruby.ast.NodeTypes;
042: import org.jruby.ast.YieldNode;
043: import org.jruby.lexer.yacc.ISourcePosition;
044: import org.netbeans.modules.gsf.api.CompilationInfo;
045: import org.netbeans.modules.gsf.api.OffsetRange;
046: import org.netbeans.api.lexer.Token;
047: import org.netbeans.api.lexer.TokenId;
048: import org.netbeans.api.lexer.TokenSequence;
049: import org.netbeans.editor.BaseDocument;
050: import org.netbeans.editor.Utilities;
051: import org.netbeans.modules.ruby.AstPath;
052: import org.netbeans.modules.ruby.AstUtilities;
053: import org.netbeans.modules.ruby.hints.spi.AstRule;
054: import org.netbeans.modules.ruby.hints.spi.Description;
055: import org.netbeans.modules.ruby.hints.spi.EditList;
056: import org.netbeans.modules.ruby.hints.spi.Fix;
057: import org.netbeans.modules.ruby.hints.spi.HintSeverity;
058: import org.netbeans.modules.ruby.hints.spi.PreviewableFix;
059: import org.netbeans.modules.ruby.hints.spi.RuleContext;
060: import org.netbeans.modules.ruby.lexer.LexUtilities;
061: import org.netbeans.modules.ruby.lexer.RubyTokenId;
062: import org.openide.util.Exceptions;
063: import org.openide.util.NbBundle;
064:
065: /**
066: * Offer to convert a {}-style block into do-end, or vice versa
067: *
068: * @author Tor Norbye
069: */
070: public class ConvertBlockType implements AstRule {
071:
072: public ConvertBlockType() {
073: }
074:
075: public boolean appliesTo(CompilationInfo info) {
076: // Skip for RHTML files for now - isn't implemented properly
077: return info.getFileObject().getMIMEType().equals("text/x-ruby");
078: }
079:
080: public Set<Integer> getKinds() {
081: return Collections.singleton(NodeTypes.ITERNODE);
082: }
083:
084: public void run(RuleContext context, List<Description> result) {
085: Node node = context.node;
086: CompilationInfo info = context.compilationInfo;
087: int caretOffset = context.caretOffset;
088: BaseDocument doc = context.doc;
089:
090: assert (node.nodeId == NodeTypes.ITERNODE);
091: try {
092: int astOffset = node.getPosition().getStartOffset();
093: int lexOffset = LexUtilities
094: .getLexerOffset(info, astOffset);
095: if (lexOffset == -1 || lexOffset > doc.getLength() - 1) {
096: return;
097: }
098:
099: // Limit the hint to the -opening- line of the block
100: boolean caretOnStart = true;
101: final int beginRowEnd = Utilities.getRowEnd(doc, lexOffset);
102: final int caretRowEnd = Utilities.getRowEnd(doc,
103: caretOffset);
104: boolean caretLine = beginRowEnd == caretRowEnd;
105: int endLexOffset = -1;
106: if (!caretLine) {
107: // ...or the -ending- line of the block
108: int endAstOffset = node.getPosition().getEndOffset();
109: endLexOffset = LexUtilities.getLexerOffset(info,
110: endAstOffset);
111: if (endLexOffset == -1) {
112: return;
113: }
114: int endRowEnd = endLexOffset;
115: if (endRowEnd < doc.getLength()) {
116: endRowEnd = Utilities.getRowEnd(doc, endLexOffset);
117: }
118: caretLine = endRowEnd == caretRowEnd;
119: if (!caretLine) {
120: return;
121: }
122: if (endRowEnd != beginRowEnd) {
123: caretOnStart = false;
124: }
125: }
126:
127: Token<? extends RubyTokenId> token = LexUtilities.getToken(
128: doc, lexOffset);
129: if (token == null) {
130: return;
131: }
132:
133: TokenId id = token.id();
134: if (id == RubyTokenId.LBRACE || id == RubyTokenId.DO) {
135: OffsetRange range;
136: if (caretOnStart) {
137: range = new OffsetRange(lexOffset, lexOffset
138: + token.length());
139: } else {
140: assert endLexOffset != -1;
141: int len = (id == RubyTokenId.LBRACE) ? 1 : 3; // }=1, end=3
142: range = new OffsetRange(endLexOffset - len,
143: endLexOffset);
144: }
145: List<Fix> fixList = new ArrayList<Fix>(1);
146: boolean convertFromBrace = id == RubyTokenId.LBRACE;
147:
148: int endOffset = node.getPosition().getEndOffset();
149: if (endOffset > doc.getLength()) {
150: endOffset = doc.getLength();
151: }
152:
153: // See if we should offer to collapse
154: String text = doc.getText(lexOffset, endOffset
155: - lexOffset);
156: int nonspaceChars = 0;
157: for (int i = 0; i < text.length(); i++) {
158: char c = text.charAt(i);
159: if (!Character.isWhitespace(c)) {
160: nonspaceChars++;
161: }
162: }
163: int startColumn = lexOffset
164: - Utilities.getRowStart(doc, lexOffset);
165: // Not yet exposed from the Ruby module
166: //int rightMargin = org.netbeans.modules.ruby.options.CodeStyle.getDefault(null).getRightMargin();
167: // #119151: This should be available for a lot of hints that don't neatly fit.
168: // So only suppress it for -really- large blocks.
169: int rightMargin = 350;
170: boolean offerCollapse = rightMargin > startColumn
171: + nonspaceChars;
172:
173: // TODO - in an RHTML page, make sure there are no "gaps" (non Ruby code) between the do and the end,
174: // since we can't handle those for collapse
175: // TODO
176:
177: boolean sameLine = Utilities.getRowEnd(doc, lexOffset) == Utilities
178: .getRowEnd(doc, endOffset);
179: if (sameLine && convertFromBrace) {
180: fixList.add(new ConvertTypeFix(info, node,
181: convertFromBrace, !convertFromBrace, true,
182: false));
183: } else if (!sameLine && !convertFromBrace
184: && offerCollapse) {
185: fixList.add(new ConvertTypeFix(info, node,
186: convertFromBrace, !convertFromBrace, false,
187: true));
188: } // else: Should I let you expand a single line do-end to a multiline {}, or vice versa? Naeh,
189: // they can do this in two steps; it's not common
190: fixList.add(new ConvertTypeFix(info, node,
191: convertFromBrace, !convertFromBrace, false,
192: false));
193: if (sameLine || (!sameLine && offerCollapse)) {
194: fixList.add(new ConvertTypeFix(info, node, false,
195: false, sameLine, !sameLine));
196: }
197: Description desc = new Description(this ,
198: getDisplayName(), info.getFileObject(), range,
199: fixList, 500);
200: result.add(desc);
201: }
202: } catch (BadLocationException ex) {
203: Exceptions.printStackTrace(ex);
204: }
205: }
206:
207: public String getId() {
208: return "Convert_Blocktype"; // NOI18N
209: }
210:
211: public String getDisplayName() {
212: return NbBundle.getMessage(ConvertBlockType.class,
213: "ConvertBlockType");
214: }
215:
216: public String getDescription() {
217: return NbBundle.getMessage(ConvertBlockType.class,
218: "ConvertBlockTypeDesc");
219: }
220:
221: public boolean getDefaultEnabled() {
222: return true;
223: }
224:
225: public HintSeverity getDefaultSeverity() {
226: return HintSeverity.CURRENT_LINE_WARNING;
227: }
228:
229: public JComponent getCustomizer(Preferences node) {
230: return null;
231: }
232:
233: public boolean showInTasklist() {
234: return false;
235: }
236:
237: private static class ConvertTypeFix implements PreviewableFix {
238:
239: private final CompilationInfo info;
240: private final boolean convertToDo;
241: private final boolean convertToBrace;
242: private final Node node;
243: private final boolean expand;
244: private final boolean collapse;
245:
246: ConvertTypeFix(CompilationInfo info, Node node,
247: boolean convertToDo, boolean convertToBrace,
248: boolean expand, boolean collapse) {
249: this .info = info;
250: this .node = node;
251: this .convertToDo = convertToDo;
252: this .convertToBrace = convertToBrace;
253: this .expand = expand;
254: this .collapse = collapse;
255: }
256:
257: public String getDescription() {
258: String key;
259: if (convertToDo) {
260: if (expand) {
261: key = "ConvertBraceToDoMulti"; // NOI18N
262: } else if (collapse) {
263: key = "ConvertBraceToDoSingle"; // NOI18N
264: } else {
265: key = "ConvertBraceToDo"; // NOI18N
266: }
267: } else if (convertToBrace) {
268: if (expand) {
269: key = "ConvertDoToBraceMulti"; // NOI18N
270: } else if (collapse) {
271: key = "ConvertDoToBraceSingle"; // NOI18N
272: } else {
273: key = "ConvertDoToBrace"; // NOI18N
274: }
275: } else {
276: if (expand) {
277: key = "ChangeBlockToMulti"; // NOI18N
278: } else {
279: assert collapse;
280: key = "ChangeBlockToSingle"; // NOI18N
281: }
282: }
283: return NbBundle.getMessage(ConvertBlockType.class, key);
284: }
285:
286: public boolean canPreview() {
287: return true;
288: }
289:
290: public void implement() throws Exception {
291: getEditList().apply();
292: }
293:
294: public EditList getEditList() throws Exception {
295: BaseDocument doc = (BaseDocument) info.getDocument();
296: EditList edits = new EditList(doc);
297:
298: ISourcePosition pos = node.getPosition();
299: int startOffset = pos.getStartOffset();
300: int originalEnd = pos.getEndOffset();
301: int endOffset;
302: if (convertToDo) {
303: endOffset = originalEnd - 1;
304: } else if (convertToBrace) {
305: endOffset = originalEnd - 3;
306: } else {
307: endOffset = originalEnd;
308: }
309: if (startOffset > doc.getLength() - 1
310: || endOffset > doc.getLength()) {
311: return edits;
312: }
313:
314: if (convertToDo) {
315: if (doc.getText(startOffset, 1).charAt(0) == '{'
316: && doc.getText(endOffset, 1).charAt(0) == '}') {
317: String end;
318: if (endOffset > 0
319: && !Character.isWhitespace(doc.getText(
320: endOffset - 1, 1).charAt(0))) {
321: end = " end"; // NOI18N
322: } else {
323: end = "end"; // NOI18N
324: }
325: edits.replace(endOffset, 1, end, false, 0); // NOI18N
326:
327: boolean spaceBefore = true;
328: boolean spaceAfter = true;
329: if (startOffset > 0) {
330: String s = doc.getText(startOffset - 1, 3);
331: spaceBefore = Character.isWhitespace(s
332: .charAt(0));
333: spaceAfter = Character
334: .isWhitespace(s.charAt(2));
335: }
336: String insert = "do";
337: if (!spaceAfter) {
338: insert = insert + " ";
339: }
340: if (!spaceBefore) {
341: insert = " " + insert;
342: }
343: edits.replace(startOffset, 1, insert, false, 1); // NOI18N
344:
345: if (expand) {
346: expand(edits, doc, node, startOffset,
347: originalEnd);
348: } else if (collapse) {
349: collapse(edits, doc, node, startOffset,
350: originalEnd);
351: }
352: }
353: } else if (convertToBrace) {
354: if (doc.getText(startOffset, 2).equals("do")
355: && endOffset <= doc.getLength() - 3 && // NOI18N
356: doc.getText(endOffset, 3).equals("end")) { // NOI18N
357: // TODO - make sure there is whitespace next to these tokens!!!
358: // They are optional around {} but not around do/end!
359: AstPath path = new AstPath(AstUtilities
360: .getRoot(info), node);
361: assert path.leaf() == node;
362: boolean parenIsNecessary = isArgParenNecessary(
363: path, doc);
364:
365: edits.replace(endOffset, 3, "}", false, 0); // NOI18N
366: edits.replace(startOffset, 2, "{", false, 0); // NOI18N
367:
368: if (parenIsNecessary) {
369: // Insert parentheses
370: assert AstUtilities.isCall(path.leafParent());
371: OffsetRange range = AstUtilities
372: .getCallRange(path.leafParent());
373: int insertPos = range.getEnd();
374: // Check if I should remove a space; e.g. replace "foo arg" with "foo(arg"
375: if (Character.isWhitespace(doc.getText(
376: insertPos, 1).charAt(0))) {
377: edits.replace(insertPos, 1, "(", false, 1); // NOI18N
378: } else {
379: edits.replace(insertPos, 0, "(", false, 1); // NOI18N
380: }
381:
382: // Insert )
383: edits
384: .replace(startOffset - 1, 0, ")",
385: false, 2); // NOI18N
386:
387: if (!Character.isWhitespace(doc.getText(
388: startOffset - 1, 1).charAt(0))) {
389: edits.replace(startOffset - 1, 0, " ",
390: false, 3); // NOI18N
391: }
392: }
393:
394: if (expand) {
395: expand(edits, doc, node, startOffset,
396: originalEnd);
397: } else if (collapse) {
398: collapse(edits, doc, node, startOffset,
399: originalEnd);
400: }
401: }
402: } else {
403: assert collapse || expand;
404:
405: if (expand) {
406: expand(edits, doc, node, startOffset, endOffset);
407: } else {
408: collapse(edits, doc, node, startOffset, endOffset);
409: }
410: }
411:
412: return edits;
413: }
414:
415: /** JRuby sometimes has wrong AST offsets. For example, for
416: * this IterNode
417: * sort{|a1, a2| a1[0].id2name <=> a2[0].id2name}
418: * the NewlineNode inside the iter will be here: a1^[0] instead of ^a1[0].
419: * To work around this problem, look at the left most children of a NewlineNode
420: * and find the TRUE starting range of the newline node.
421: * @todo File JRuby issue
422: */
423: private int findRealStart(Node node) {
424: int min = Integer.MAX_VALUE;
425: while (true) {
426: int start = node.getPosition().getStartOffset();
427: if (start < min) {
428: min = start;
429: }
430:
431: @SuppressWarnings(value="unchecked")
432: List<Node> list = node.childNodes();
433:
434: if (list != null && list.size() > 0) {
435: node = list.get(0);
436: } else {
437: return min;
438: }
439: }
440: }
441:
442: private void findLineBreaks(Node node, Set<Integer> offsets) {
443: if (node.nodeId == NodeTypes.NEWLINENODE) {
444: // Doesn't work, need above workaround
445: //int start = node.getPosition().getStartOffset();
446: int start = findRealStart(node);
447: offsets.add(start);
448: }
449:
450: @SuppressWarnings(value="unchecked")
451: List<Node> list = node.childNodes();
452:
453: for (Node child : list) {
454: if (child.nodeId == NodeTypes.EVSTRNODE) {
455: // Don't linebreak inside a #{} expression
456: continue;
457: }
458: findLineBreaks(child, offsets);
459: }
460: }
461:
462: /** NOTE - document should be under atomic lock when this is called */
463: private void expand(EditList edits, BaseDocument doc,
464: Node node, int startOffset, int endOffset) {
465: assert endOffset <= doc.getLength();
466:
467: // Look through the document and find the statement separators (;);
468: // at these locations I'll replace the ; with a newline and then
469: // apply a formatter
470: Set<Integer> offsetSet = new HashSet<Integer>();
471: findLineBreaks(node, offsetSet);
472:
473: // Add in ; replacements
474: TokenSequence<? extends RubyTokenId> ts = LexUtilities
475: .getRubyTokenSequence(doc, endOffset);
476: if (ts != null) {
477: // Traverse sequence in reverse order such that my offset list is in decreasing order
478: ts.move(endOffset);
479: while (ts.movePrevious() && ts.offset() > startOffset) {
480: Token<? extends RubyTokenId> token = ts.token();
481: TokenId id = token.id();
482:
483: if (id == RubyTokenId.IDENTIFIER
484: && ";".equals(token.text().toString())) { // NOI18N
485: //offsetSet.add(ts.offset());
486: } else if (id == RubyTokenId.END
487: || id == RubyTokenId.RBRACE) {
488: offsetSet.add(ts.offset());
489: }
490: }
491: }
492:
493: List<Integer> offsets = new ArrayList<Integer>(offsetSet);
494: Collections.sort(offsets);
495: // Ensure that we go in high to lower order such that I edit the
496: // document from bottom to top (so offsets don't have to be adjusted
497: // to account for our own edits along the way)
498: Collections.reverse(offsets);
499:
500: if (offsets.size() > 0) {
501: // TODO: Create a ModificationResult here and process it
502: // The following is the WRONG way to do it...
503: // I've gotta use a ModificationResult instead!
504: try {
505: // Process offsets from back to front such that I can
506: // modify the document without worrying that the other offsets
507: // need to be adjusted
508: int prev = -1;
509: int added = 0;
510: for (int offset : offsets) {
511: // We might get some dupes since we add offsets from both
512: // the AST newline nodes and semicolons discovered in the lexical token hierarchy
513: if (offset == prev) {
514: continue;
515: }
516: prev = offset;
517:
518: // Back up over any whitespace
519: int whitespaces = 0;
520: for (int i = 1; i < 5 && offset - i > 0; i++) {
521: char c = doc.getText(offset - i, 1).charAt(
522: 0);
523: if (Character.isWhitespace(c)) {
524: whitespaces++;
525: } else {
526: break;
527: }
528: }
529:
530: if (whitespaces > 0) {
531: edits.replace(offset - whitespaces,
532: whitespaces, "\n", false, 4); // NOI18N
533: } else {
534: edits.replace(offset, 0, "\n", false, 4); // NOI18N
535: }
536: added++;
537: }
538:
539: // Remove trailing semicolons
540: for (int offset : offsets) {
541: char c = doc.getText(offset - 1, 1).charAt(0);
542: if (c == ';') {
543: edits
544: .replace(offset - 1, 1, null,
545: false, 5);
546: } else if (Character.isWhitespace(c)) {
547: c = doc.getText(offset - 2, 1).charAt(0);
548: if (c == ';') {
549: edits.replace(offset - 2, 1, null,
550: false, 5);
551: }
552: }
553: }
554: int newEnd = endOffset + added;
555:
556: // Remove trailing whitespace
557: // TODO
558:
559: } catch (BadLocationException ble) {
560: Exceptions.printStackTrace(ble);
561: }
562: }
563: edits.format();
564: }
565:
566: private void collapse(EditList edits, BaseDocument doc,
567: Node node, int startOffset, int endOffset) {
568: assert endOffset <= doc.getLength();
569:
570: // Look through the document and find the statement separators (;);
571: // at these locations I'll replace the ; with a newline and then
572: // apply a formatter
573: Set<Integer> offsetSet = new HashSet<Integer>();
574: findLineBreaks(node, offsetSet);
575:
576: Token<? extends TokenId> t = LexUtilities.getToken(doc,
577: startOffset);
578: TokenId tid = t.id();
579: assert tid == RubyTokenId.LBRACE || tid == RubyTokenId.DO;
580: boolean isDoBlock = tid == RubyTokenId.DO;
581:
582: // Add in ; replacements
583: TokenSequence<? extends RubyTokenId> ts = LexUtilities
584: .getRubyTokenSequence(doc, endOffset);
585: if (ts != null) {
586: // Traverse sequence in reverse order such that my offset list is in decreasing order
587: ts.move(endOffset);
588: while (ts.movePrevious() && ts.offset() > startOffset) {
589: Token<? extends RubyTokenId> token = ts.token();
590: TokenId id = token.id();
591:
592: if (id == RubyTokenId.END
593: || id == RubyTokenId.RBRACE) {
594: offsetSet.add(ts.offset());
595: }
596: }
597: }
598:
599: List<Integer> offsets = new ArrayList<Integer>(offsetSet);
600: Collections.sort(offsets);
601: // Ensure that we go in high to lower order such that I edit the
602: // document from bottom to top (so offsets don't have to be adjusted
603: // to account for our own edits along the way)
604: //Collections.reverse(offsets);
605: if (offsets.size() > 0) {
606: // TODO: Create a ModificationResult here and process it
607: // The following is the WRONG way to do it...
608: // I've gotta use a ModificationResult instead!
609: try {
610: // Process offsets from back to front such that I can
611: // modify the document without worrying that the other offsets
612: // need to be adjusted
613: int prev = -1;
614: //int posDelta; // Amount to add to offsets to account for our
615: for (int i = offsets.size() - 1; i >= 0; i--) {
616: int offset = offsets.get(i);
617: // We might get some dupes since we add offsets from both
618: // the AST newline nodes and semicolons discovered in the lexical token hierarchy
619: if (offset == prev) {
620: continue;
621: }
622: prev = offset;
623: int prevOffset = i > 0 ? offsets.get(i - 1) : 0;
624:
625: int segmentOffset = offset;
626: // TODO - use an editor-finder which can do this efficiently
627: // See also DocumentUtilities.getText() which can do it efficiently
628: int s = segmentOffset;
629: while (s > prevOffset) {
630: s--;
631: char c = doc.getText(s, 1).charAt(0);
632: if (Character.isWhitespace(c)) {
633: segmentOffset = s;
634: } else {
635: break;
636: }
637: }
638: int segmentLength = offset - segmentOffset;
639: s = offset - 1;
640: while (s < doc.getLength()) {
641: s++;
642: char c = doc.getText(s, 1).charAt(0);
643: if (Character.isWhitespace(c)) {
644: segmentLength++;
645: } else {
646: break;
647: }
648: }
649:
650: // Collapse all whitespace around this offset and replace with a single "; "
651: char prevChar = '?';
652: if (segmentOffset > 0) {
653: prevChar = doc
654: .getText(segmentOffset - 1, 1)
655: .charAt(0);
656: }
657: if (prevChar == '|'
658: || (isDoBlock
659: && (segmentOffset <= startOffset + 3) || (!isDoBlock && (segmentOffset <= startOffset + 1)))) {
660: edits.replace(segmentOffset, segmentLength,
661: " ", false, 4);
662: } else {
663: // Don't insert semicolons before "end" or around parens in "if (true)" etc.
664: boolean skipSemicolon = false;
665: //if (segmentOffset > 0) {
666: // Token tkr = LexUtilities.getToken(doc, segmentOffset-1);
667: // if (tkr != null && tkr.id() == RubyTokenId.RPAREN) {
668: // skipSemicolon = true;
669: // }
670: //}
671: TokenSequence<? extends TokenId> rts = LexUtilities
672: .getRubyTokenSequence(doc,
673: segmentOffset);
674: rts.move(segmentOffset);
675: while (rts.moveNext()) {
676: Token tk = rts.token();
677: TokenId tkid = tk.id();
678: if (tkid == RubyTokenId.END
679: || tkid == RubyTokenId.RBRACE
680: || tkid == RubyTokenId.LPAREN) {
681: skipSemicolon = true;
682: break;
683: } else if (tkid != RubyTokenId.WHITESPACE) {
684: break;
685: }
686: }
687: if (skipSemicolon) {
688: edits.replace(segmentOffset,
689: segmentLength, " ", false, 4);
690: } else {
691: edits.replace(segmentOffset,
692: segmentLength, "; ", false, 4);
693: }
694: }
695: }
696: } catch (BadLocationException ble) {
697: Exceptions.printStackTrace(ble);
698: }
699: }
700: edits.format();
701: }
702:
703: /** Determine whether parentheses are necessary around the call
704: * corresponding to a block call.
705: * For example, in
706: * <pre>
707: * b.create_menu :name => 'default_menu' do |d| ...
708: * </pre>
709: * parens are necessary if you want to switch to a brace block.
710: */
711: private boolean isArgParenNecessary(AstPath path,
712: BaseDocument doc) throws BadLocationException {
713: // Look at the surrounding CallNode and see if it has arguments.
714: // If so, see if it has parens. If not, return true.
715: assert path.leaf().nodeId == NodeTypes.ITERNODE;
716: Node n = path.leafParent();
717: if (n != null && AstUtilities.isCall(n)
718: && n instanceof IArgumentNode
719: && ((IArgumentNode) n).getArgsNode() != null) {
720: // Yes, call has args - check parens
721: int end = node.getPosition().getStartOffset(); // Start of do/{ - end of args
722: for (int i = end - 1; i >= 0 && i < doc.getLength(); i--) {
723: // XXX Use a more performant document content iterator!
724: char c = doc.getText(i, 1).charAt(0);
725: if (Character.isWhitespace(c)) {
726: continue;
727: }
728: if (c == ')') {
729: return false;
730: } else {
731: return true;
732: }
733: }
734: }
735:
736: return false;
737: }
738:
739: public boolean isSafe() {
740: // Different precedence rules apply for do and {}
741: return !convertToBrace && !convertToDo;
742: }
743:
744: public boolean isInteractive() {
745: return false;
746: }
747: }
748: }
|