0001: /*
0002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
0003: *
0004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
0005: *
0006: * The contents of this file are subject to the terms of either the GNU
0007: * General Public License Version 2 only ("GPL") or the Common
0008: * Development and Distribution License("CDDL") (collectively, the
0009: * "License"). You may not use this file except in compliance with the
0010: * License. You can obtain a copy of the License at
0011: * http://www.netbeans.org/cddl-gplv2.html
0012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
0013: * specific language governing permissions and limitations under the
0014: * License. When distributing the software, include this License Header
0015: * Notice in each file and include the License file at
0016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
0017: * particular file as subject to the "Classpath" exception as provided
0018: * by Sun in the GPL Version 2 section of the License file that
0019: * accompanied this code. If applicable, add the following below the
0020: * License Header, with the fields enclosed by brackets [] replaced by
0021: * your own identifying information:
0022: * "Portions Copyrighted [year] [name of copyright owner]"
0023: *
0024: * Contributor(s):
0025: *
0026: * The Original Software is NetBeans. The Initial Developer of the Original
0027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
0028: * Microsystems, Inc. All Rights Reserved.
0029: *
0030: * If you wish your version of this file to be governed by only the CDDL
0031: * or only the GPL Version 2, indicate your decision by adding
0032: * "[Contributor] elects to include this software in this distribution
0033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
0034: * single choice of license, a recipient has the option to distribute
0035: * your version of this file under either the CDDL, the GPL Version 2 or
0036: * to extend the choice of license to its licensees as provided above.
0037: * However, if you add GPL Version 2 code and therefore, elected the GPL
0038: * Version 2 license, then the option applies only if the new code is
0039: * made subject to such option by the copyright holder.
0040: */
0041: package org.netbeans.modules.javascript.editing;
0042:
0043: import java.util.ArrayList;
0044: import java.util.HashSet;
0045: import java.util.List;
0046: import java.util.Set;
0047: import java.util.Stack;
0048: import javax.swing.text.BadLocationException;
0049: import javax.swing.text.Document;
0050: import org.netbeans.api.lexer.Token;
0051: import org.netbeans.api.lexer.TokenHierarchy;
0052: import org.netbeans.api.lexer.TokenId;
0053: import org.netbeans.api.lexer.TokenSequence;
0054: import org.netbeans.editor.BaseDocument;
0055: import org.netbeans.editor.Utilities;
0056: import org.netbeans.modules.gsf.api.CompilationInfo;
0057: import org.netbeans.modules.gsf.api.OffsetRange;
0058: import org.netbeans.modules.javascript.editing.lexer.LexUtilities;
0059: import org.netbeans.modules.javascript.editing.lexer.JsTokenId;
0060: import org.openide.util.Exceptions;
0061:
0062: /**
0063: * Formatting and indentation for JavaScript
0064: *
0065: * @todo Handle JSP
0066: * @todo Handle if-blocks that don't have an associated block - just indents the next statement
0067: * @todo Handle block comments - similar to multiline literals but should be indented by a relative amount
0068: * @todo Handle XML/E4X content
0069: * @todo Use the Context.modifyIndent() method to change line indents instead of
0070: * the current document/formatter method
0071: * @todo Indent block comments properly: See if the first char is "*", and if so, indent it one extra
0072: * char somehow such that it lines up with the * in /*
0073: *
0074: * @author Tor Norbye
0075: */
0076: public class JsFormatter implements
0077: org.netbeans.modules.gsf.api.Formatter {
0078: private boolean embeddedJavaScript;
0079: private CodeStyle codeStyle;
0080: private int rightMarginOverride = -1;
0081:
0082: /**
0083: * <p>
0084: * Stack describing indentation of blocks defined by '{', '[' and blocks
0085: * with missing optional curly braces '{'. See also getBracketBalanceDelta()
0086: * </p>
0087: * For example:
0088: * <pre>
0089: * if (true) // [ StackItem[block=true] ]
0090: * if (true) { // [ StackItem[block=true], StackItem[block=false] ]
0091: * if (true) // [ StackItem[block=true], StackItem[block=false], StackItem[block=true] ]
0092: * foo(); // [ StackItem[block=true], StackItem[block=false] ]
0093: * bar(); // [ StackItem[block=true], StackItem[block=false] ]
0094: * } // [ StackItem[block=true] ]
0095: * fooBar(); // [ ]
0096: * </pre>
0097: */
0098: private Stack<StackItem> stack = new Stack<StackItem>();
0099:
0100: public JsFormatter() {
0101: this .codeStyle = CodeStyle.getDefault(null);
0102: }
0103:
0104: public JsFormatter(CodeStyle codeStyle, int rightMarginOverride) {
0105: assert codeStyle != null;
0106: this .codeStyle = codeStyle;
0107: this .rightMarginOverride = rightMarginOverride;
0108: }
0109:
0110: public boolean needsParserResult() {
0111: return true;
0112: }
0113:
0114: public void reformat(Document document, int startOffset,
0115: int endOffset, CompilationInfo info) {
0116:
0117: reindent(document, startOffset, endOffset, info, false);
0118: }
0119:
0120: public void reindent(Document document, int startOffset,
0121: int endOffset) {
0122: reindent(document, startOffset, endOffset, null, true);
0123: }
0124:
0125: public int indentSize() {
0126: return codeStyle.getIndentSize();
0127: }
0128:
0129: public int hangingIndentSize() {
0130: return codeStyle.getContinuationIndentSize();
0131: }
0132:
0133: /** Compute the initial balance of brackets at the given offset. */
0134: private int getFormatStableStart(BaseDocument doc, int offset) {
0135: TokenSequence<? extends JsTokenId> ts = LexUtilities
0136: .getJsTokenSequence(doc, offset);
0137: if (ts == null) {
0138: return 0;
0139: }
0140:
0141: ts.move(offset);
0142:
0143: if (!ts.movePrevious()) {
0144: return 0;
0145: }
0146:
0147: // Look backwards to find a suitable context - a class, module or method definition
0148: // which we will assume is properly indented and balanced
0149: do {
0150: Token<? extends JsTokenId> token = ts.token();
0151: TokenId id = token.id();
0152:
0153: if (id == JsTokenId.FUNCTION) {
0154: return ts.offset();
0155: }
0156: } while (ts.movePrevious());
0157:
0158: if (embeddedJavaScript && !ts.movePrevious()) {
0159: // I may have moved to the front of an embedded JavaScript area, e.g. in
0160: // an attribute or in a <script> tag. If this is the end of the line,
0161: // go to the next line instead since the reindent code will go to the beginning
0162: // of the stable formatting start.
0163: int sequenceBegin = ts.offset();
0164: try {
0165: int lineTextEnd = Utilities.getRowLastNonWhite(doc,
0166: sequenceBegin);
0167: if (lineTextEnd == -1 || sequenceBegin > lineTextEnd) {
0168: return Math.min(doc.getLength(), Utilities
0169: .getRowEnd(doc, sequenceBegin) + 1);
0170: }
0171:
0172: } catch (BadLocationException ble) {
0173: Exceptions.printStackTrace(ble);
0174: }
0175: }
0176:
0177: return ts.offset();
0178: }
0179:
0180: private static int getPreviousLineFirstNonWhiteOffset(
0181: BaseDocument doc, int offset) {
0182: int offsetPrevLine = -1;
0183: try {
0184: if (offset > -1) {
0185: int o = Utilities.getRowStart(doc, offset);
0186: if (o > 0) {
0187: offsetPrevLine = Utilities.getRowStart(doc, o - 1);
0188: if (offsetPrevLine > -1) {
0189: offsetPrevLine = Utilities.getRowFirstNonWhite(
0190: doc, offsetPrevLine);
0191: }
0192: }
0193: }
0194: } catch (BadLocationException ex) {
0195: Exceptions.printStackTrace(ex);
0196: }
0197: return offsetPrevLine;
0198: }
0199:
0200: private int getBracketBalanceDelta(TokenId id) {
0201: if (id == JsTokenId.LPAREN || id == JsTokenId.LBRACKET) {
0202: return 1;
0203: } else if (id == JsTokenId.RPAREN || id == JsTokenId.RBRACKET) {
0204: return -1;
0205: }
0206: return 0;
0207: }
0208:
0209: private int getTokenBalanceDelta(TokenId id, BaseDocument doc,
0210: TokenSequence<? extends JsTokenId> ts) {
0211: try {
0212: if (id == JsTokenId.LBRACKET || id == JsTokenId.LBRACE) {
0213: // block with braces, just record it to stack and return 1
0214: stack.push(new StackItem(false, new OffsetRange(ts
0215: .offset(), ts.offset())));
0216: return 1;
0217: } else if (id == JsTokenId.RBRACKET
0218: || id == JsTokenId.RBRACE) {
0219: /*
0220: * End of braces block.
0221: * If we are not on same line where block started, try to push
0222: * all braceless blocks from stack and decrease indent for them,
0223: * otherwise just decrese indent by 1.
0224: * For example:
0225: * if (true)
0226: * if (true)
0227: * if (true)
0228: * foo(); // we should decrease indent by 3 levels
0229: *
0230: * but:
0231: * if (true)
0232: * if (true)
0233: * if (map[0]) // at ']' we should decrease only by 1
0234: * foo();
0235: */
0236: int delta = -1;
0237: StackItem lastPop = stack.empty() ? null : stack.pop();
0238: if (lastPop != null
0239: && Utilities.getLineOffset(doc, lastPop.range
0240: .getStart()) != Utilities
0241: .getLineOffset(doc, ts.offset())) {
0242: int blocks = 0;
0243: while (!stack.empty() && stack.pop().braceless) {
0244: blocks++;
0245: }
0246: delta -= blocks;
0247: }
0248: return delta;
0249: } else if (LexUtilities.getMultilineRange(doc, ts.offset()) != OffsetRange.NONE) {
0250: // we found braceless block, let's record it in the stack
0251: stack.push(new StackItem(true, LexUtilities
0252: .getMultilineRange(doc, ts.offset())));
0253: } else if (id == JsTokenId.EOL) {
0254: if (!stack.empty()) {
0255: if (stack.peek().braceless) {
0256: // end of line after braceless block start
0257: OffsetRange stackOffset = stack.peek().range;
0258: if (stackOffset.containsInclusive(ts.offset())) {
0259: // we are in the braceless block statement
0260: int stackEndLine = Utilities.getLineOffset(
0261: doc, stackOffset.getEnd());
0262: int offsetLine = Utilities.getLineOffset(
0263: doc, ts.offset());
0264: if (stackEndLine == offsetLine) {
0265: // if we are at the last line of braceless block statement
0266: // increse indent by 1
0267: return 1;
0268: }
0269: } else {
0270: // we are not in braceless block statement,
0271: // let's decrease indent for all braceless blocks in top of stack (if any)
0272: int blocks = 0;
0273: while (!stack.empty()
0274: && stack.peek().braceless) {
0275: blocks++;
0276: stack.pop();
0277: }
0278: return -blocks;
0279: }
0280: }
0281: }
0282: }
0283: } catch (BadLocationException ble) {
0284: Exceptions.printStackTrace(ble);
0285: }
0286: return 0;
0287: }
0288:
0289: // TODO RHTML - there can be many discontiguous sections, I've gotta process all of them on the given line
0290: private int getTokenBalance(BaseDocument doc, int begin, int end,
0291: boolean includeKeywords, Set<OffsetRange> ranges) {
0292: int balance = 0;
0293:
0294: if (embeddedJavaScript) {
0295: TokenHierarchy<Document> th = TokenHierarchy
0296: .get((Document) doc);
0297: // Probably an RHTML file - gotta process it in sections since I can have lines
0298: // made up of both whitespace, ruby, html and delimiters and all ruby sections
0299: // can affect the token balance
0300: TokenSequence<?> t = th.tokenSequence();
0301: if (t == null) {
0302: return 0;
0303: }
0304: t.move(begin);
0305: if (!t.moveNext()) {
0306: return 0;
0307: }
0308:
0309: do {
0310: Token<?> token = t.token();
0311: TokenId id = token.id();
0312: if (id.primaryCategory().equals("html")) { // NOI18N
0313: // Some kind of "top level" language like RHTML which is two
0314: // levels away from JavaScript...
0315: TokenSequence<?> hts = t.embedded();
0316: hts.move(begin);
0317: hts.moveNext();
0318: do {
0319: Token<?> htmlToken = hts.token();
0320: if (htmlToken == null) {
0321: break;
0322: }
0323: TokenId htmlId = htmlToken.id();
0324: if (htmlId.primaryCategory().equals("script")) {
0325: TokenSequence<? extends JsTokenId> ts = hts
0326: .embedded(JsTokenId.language());
0327: ts.move(begin);
0328: ts.moveNext();
0329:
0330: do {
0331: Token<? extends JsTokenId> jsToken = ts
0332: .token();
0333: if (jsToken == null) {
0334: break;
0335: }
0336: TokenId jsId = jsToken.id();
0337:
0338: if (includeKeywords) {
0339: balance += getTokenBalanceDelta(
0340: jsId, doc, ts);
0341: } else {
0342: balance += getBracketBalanceDelta(jsId);
0343: }
0344: } while (ts.moveNext()
0345: && (ts.offset() < end));
0346: }
0347: } while (hts.moveNext() && (hts.offset() < end));
0348: } else if (id.primaryCategory().equals("script")) { // NOI18N
0349: TokenSequence<? extends JsTokenId> ts = t
0350: .embedded(JsTokenId.language());
0351: ts.move(begin);
0352: ts.moveNext();
0353:
0354: do {
0355: Token<? extends JsTokenId> jsToken = ts.token();
0356: if (jsToken == null) {
0357: break;
0358: }
0359: TokenId jsId = jsToken.id();
0360:
0361: if (includeKeywords) {
0362: balance += getTokenBalanceDelta(jsId, doc,
0363: ts);
0364: } else {
0365: balance += getBracketBalanceDelta(jsId);
0366: }
0367: } while (ts.moveNext() && (ts.offset() < end));
0368: }
0369:
0370: } while (t.moveNext() && (t.offset() < end));
0371: } else {
0372: TokenSequence<? extends JsTokenId> ts = LexUtilities
0373: .getJsTokenSequence(doc, begin);
0374: if (ts == null) {
0375: return 0;
0376: }
0377:
0378: ts.move(begin);
0379:
0380: if (!ts.moveNext()) {
0381: return 0;
0382: }
0383:
0384: do {
0385: Token<? extends JsTokenId> token = ts.token();
0386: TokenId id = token.id();
0387:
0388: if (includeKeywords) {
0389: balance += getTokenBalanceDelta(id, doc, ts);
0390: } else {
0391: balance += getBracketBalanceDelta(id);
0392: }
0393: } while (ts.moveNext() && (ts.offset() < end));
0394: }
0395:
0396: return balance;
0397: }
0398:
0399: /**
0400: * Get the first token on the given line. Similar to LexUtilities.getToken(doc, lineBegin)
0401: * except (a) it computes the line begin from the offset itself, and more importantly,
0402: * (b) it handles RHTML tokens specially; e.g. if a line begins with
0403: * {@code
0404: * <% if %>
0405: * }
0406: * then the "if" embedded token will be returned rather than the RHTML delimiter, or even
0407: * the whitespace token (which is the first Ruby token in the embedded sequence).
0408: *
0409: * </pre>
0410: */
0411: private Token<? extends JsTokenId> getFirstToken(BaseDocument doc,
0412: int offset) throws BadLocationException {
0413: int lineBegin = Utilities.getRowFirstNonWhite(doc, offset);
0414:
0415: if (lineBegin != -1) {
0416: if (embeddedJavaScript) {
0417: TokenSequence<? extends JsTokenId> ts = LexUtilities
0418: .getJsTokenSequence(doc, lineBegin);
0419: if (ts != null) {
0420: ts.moveNext();
0421: Token<? extends JsTokenId> token = ts.token();
0422: while (token != null
0423: && token.id() == JsTokenId.WHITESPACE) {
0424: if (!ts.moveNext()) {
0425: return null;
0426: }
0427: token = ts.token();
0428: }
0429: return token;
0430: }
0431: } else {
0432: return LexUtilities.getToken(doc, lineBegin);
0433: }
0434: }
0435:
0436: return null;
0437: }
0438:
0439: private boolean hasBlockOnLine(BaseDocument doc, int begin, int end) {
0440: int balance = 0;
0441: if (embeddedJavaScript) {
0442: TokenHierarchy<Document> th = TokenHierarchy
0443: .get((Document) doc);
0444: // Probably an RHTML file - gotta process it in sections since I can have lines
0445: // made up of both whitespace, ruby, html and delimiters and all ruby sections
0446: // can affect the token balance
0447: TokenSequence<?> t = th.tokenSequence();
0448: if (t == null) {
0449: return false;
0450: }
0451: t.move(begin);
0452: if (!t.moveNext()) {
0453: return false;
0454: }
0455:
0456: do {
0457: Token<?> token = t.token();
0458: TokenId id = token.id();
0459: if (id.primaryCategory().equals("html")) { // NOI18N
0460: // Some kind of "top level" language like RHTML which is two
0461: // levels away from JavaScript...
0462: TokenSequence<?> hts = t.embedded();
0463: hts.move(begin);
0464: hts.moveNext();
0465: do {
0466: Token<?> htmlToken = hts.token();
0467: if (htmlToken == null) {
0468: break;
0469: }
0470: TokenId htmlId = htmlToken.id();
0471: if (htmlId.primaryCategory().equals("script")) {
0472: TokenSequence<? extends JsTokenId> ts = hts
0473: .embedded(JsTokenId.language());
0474: ts.move(begin);
0475: ts.moveNext();
0476: do {
0477: Token<? extends JsTokenId> jsToken = ts
0478: .token();
0479: if (jsToken == null) {
0480: break;
0481: }
0482: TokenId jsId = jsToken.id();
0483:
0484: if (jsId == JsTokenId.LBRACE) {
0485: return true;
0486: }
0487: if (balance == 0
0488: && jsId == JsTokenId.SEMI) {
0489: return true;
0490: }
0491: if (jsId == JsTokenId.LPAREN) {
0492: balance++;
0493: } else if (jsId == JsTokenId.RPAREN) {
0494: balance--;
0495: }
0496: } while (ts.moveNext()
0497: && (ts.offset() < end));
0498: }
0499: } while (hts.moveNext() && (hts.offset() < end));
0500: } else if (id.primaryCategory().equals("script")) { // NOI18N
0501: TokenSequence<? extends JsTokenId> ts = t
0502: .embedded(JsTokenId.language());
0503: ts.move(begin);
0504: ts.moveNext();
0505: do {
0506: Token<? extends JsTokenId> jsToken = ts.token();
0507: if (jsToken == null) {
0508: break;
0509: }
0510: TokenId jsId = jsToken.id();
0511:
0512: if (jsId == JsTokenId.LBRACE) {
0513: return true;
0514: }
0515: if (balance == 0 && jsId == JsTokenId.SEMI) {
0516: return true;
0517: }
0518: if (jsId == JsTokenId.LPAREN) {
0519: balance++;
0520: } else if (jsId == JsTokenId.RPAREN) {
0521: balance--;
0522: }
0523: } while (ts.moveNext() && (ts.offset() < end));
0524: }
0525:
0526: } while (t.moveNext() && (t.offset() < end));
0527: } else {
0528: TokenSequence<? extends JsTokenId> ts = LexUtilities
0529: .getJsTokenSequence(doc, begin);
0530: if (ts == null) {
0531: return false;
0532: }
0533:
0534: ts.move(begin);
0535:
0536: if (!ts.moveNext()) {
0537: return false;
0538: }
0539:
0540: do {
0541: Token<? extends JsTokenId> token = ts.token();
0542: TokenId jsId = token.id();
0543:
0544: if (jsId == JsTokenId.LBRACE) {
0545: return true;
0546: }
0547: if (balance == 0 && jsId == JsTokenId.SEMI) {
0548: return true;
0549: }
0550: if (jsId == JsTokenId.LPAREN) {
0551: balance++;
0552: } else if (jsId == JsTokenId.RPAREN) {
0553: balance--;
0554: }
0555: } while (ts.moveNext() && (ts.offset() < end));
0556: }
0557:
0558: return false;
0559: }
0560:
0561: private int isEndIndent(BaseDocument doc, int offset)
0562: throws BadLocationException {
0563: int lineBegin = Utilities.getRowFirstNonWhite(doc, offset);
0564:
0565: if (lineBegin != -1) {
0566: Token<? extends JsTokenId> token = getFirstToken(doc,
0567: offset);
0568:
0569: if (token == null) {
0570: return 0;
0571: }
0572:
0573: TokenId id = token.id();
0574:
0575: // If the line starts with an end-marker, such as "end", "}", "]", etc.,
0576: // find the corresponding opening marker, and indent the line to the same
0577: // offset as the beginning of that line.
0578: if (/*(LexUtilities.isIndentToken(id) && !LexUtilities.isBeginToken(id, doc, offset)) || LexUtilities.isEndToken(id, doc, offset) ||*/
0579: id == JsTokenId.RBRACE || id == JsTokenId.RBRACKET
0580: || id == JsTokenId.RPAREN) {
0581: int indents = 1;
0582:
0583: // Check if there are multiple end markers here... if so increase indent level.
0584: // This should really do an iteration... for now just handling the most common
0585: // scenario in JavaScript where we have }) in object literals
0586: int lineEnd = Utilities.getRowEnd(doc, offset);
0587: int newOffset = offset;
0588: while (newOffset < lineEnd) {
0589: newOffset = newOffset + token.length();
0590: if (newOffset < doc.getLength()) {
0591: token = LexUtilities.getToken(doc, newOffset);
0592: if (token != null) {
0593: id = token.id();
0594: if (id == JsTokenId.WHITESPACE) {
0595: continue;
0596: /*} else if ((LexUtilities.isIndentToken(id) && !LexUtilities.isBeginToken(id, doc, offset)) || LexUtilities.isEndToken(id, doc, offset) ||
0597: id == JsTokenId.RBRACE || id == JsTokenId.RBRACKET || id == JsTokenId.RPAREN) {
0598: indents++;*/
0599: } else {
0600: break;
0601: }
0602: }
0603: }
0604: }
0605:
0606: return indents;
0607: }
0608: }
0609:
0610: return 0;
0611: }
0612:
0613: private static boolean isLineContinued(BaseDocument doc,
0614: int offset, int bracketBalance) throws BadLocationException {
0615: // TODO RHTML - this isn't going to work for rhtml embedded strings...
0616: offset = Utilities.getRowLastNonWhite(doc, offset);
0617: if (offset == -1) {
0618: return false;
0619: }
0620:
0621: TokenSequence<? extends JsTokenId> ts = LexUtilities
0622: .getPositionedSequence(doc, offset);
0623: Token<? extends JsTokenId> token = (ts != null ? ts.token()
0624: : null);
0625:
0626: if (token != null) {
0627: TokenId id = token.id();
0628:
0629: // http://www.netbeans.org/issues/show_bug.cgi?id=115279
0630: boolean isContinuationOperator = (id == JsTokenId.NONUNARY_OP || id == JsTokenId.DOT);
0631:
0632: if (ts.offset() == offset && token.length() > 1
0633: && token.text().toString().startsWith("\\")) {
0634: // Continued lines have different token types
0635: isContinuationOperator = true;
0636: }
0637:
0638: /* No line continuations with comma in JavaScrip - this misformats nested object literals
0639: * like those used in prototype and isn't necesary for real JavaScript code (since we
0640: * always have parentheses in parameter lists etc. to help with indentation
0641: if (token.length() == 1 && id == JsTokenId.IDENTIFIER && token.text().toString().equals(",")) {
0642: // If there's a comma it's a continuation operator, but inside arrays, hashes or parentheses
0643: // parameter lists we should not treat it as such since we'd "double indent" the items, and
0644: // NOT the first item (where there's no comma, e.g. you'd have
0645: // foo(
0646: // firstarg,
0647: // secondarg, # indented both by ( and hanging indent ,
0648: // thirdarg)
0649: if (bracketBalance == 0) {
0650: isContinuationOperator = true;
0651: }
0652: }
0653: */
0654: if (id == JsTokenId.NONUNARY_OP
0655: && ",".equals(token.text().toString())) {
0656: // If there's a comma it's a continuation operator, but inside arrays, hashes or parentheses
0657: // parameter lists we should not treat it as such since we'd "double indent" the items, and
0658: // NOT the first item (where there's no comma, e.g. you'd have
0659: // foo(
0660: // firstarg,
0661: // secondarg, # indented both by ( and hanging indent ,
0662: // thirdarg)
0663: isContinuationOperator = (bracketBalance == 0);
0664: }
0665:
0666: // if (isContinuationOperator) {
0667: // // Make sure it's not a case like this:
0668: // // alias eql? ==
0669: // // or
0670: // // def ==
0671: // token = LexUtilities.getToken(doc, Utilities.getRowFirstNonWhite(doc, offset));
0672: // if (token != null) {
0673: // id = token.id();
0674: // if (id == JsTokenId.DEF || id == JsTokenId.ANY_KEYWORD && token.text().toString().equals("alias")) { // NOI18N
0675: // return false;
0676: // }
0677: // }
0678: //
0679: // return true;
0680: // } else if (id == JsTokenId.ANY_KEYWORD) {
0681: // String text = token.text().toString();
0682: // if ("or".equals(text) || "and".equals(text)) { // NOI18N
0683: // return true;
0684: // }
0685: // }
0686:
0687: return isContinuationOperator;
0688: }
0689:
0690: return false;
0691: }
0692:
0693: private void reindent(Document document, int startOffset,
0694: int endOffset, CompilationInfo info, boolean indentOnly) {
0695: embeddedJavaScript = !JsUtils.isJsDocument(document);
0696:
0697: try {
0698: BaseDocument doc = (BaseDocument) document; // document.getText(0, document.getLength())
0699:
0700: if (indentOnly && embeddedJavaScript) {
0701: // Make sure we're not messing with indentation in HTML
0702: Token<? extends JsTokenId> token = LexUtilities
0703: .getToken(doc, startOffset);
0704: if (token == null) {
0705: return;
0706: }
0707: }
0708:
0709: syncOptions(doc, codeStyle);
0710:
0711: if (endOffset > doc.getLength()) {
0712: endOffset = doc.getLength();
0713: }
0714:
0715: startOffset = Utilities.getRowStart(doc, startOffset);
0716: int lineStart = startOffset;//Utilities.getRowStart(doc, startOffset);
0717: int initialOffset = 0;
0718: int initialIndent = 0;
0719: if (startOffset > 0) {
0720: int prevOffset = Utilities.getRowStart(doc,
0721: startOffset - 1);
0722: initialOffset = getFormatStableStart(doc, prevOffset);
0723: initialIndent = LexUtilities.getLineIndent(doc,
0724: initialOffset);
0725: }
0726:
0727: // Build up a set of offsets and indents for lines where I know I need
0728: // to adjust the offset. I will then go back over the document and adjust
0729: // lines that are different from the intended indent. By doing piecemeal
0730: // replacements in the document rather than replacing the whole thing,
0731: // a lot of things will work better: breakpoints and other line annotations
0732: // will be left in place, semantic coloring info will not be temporarily
0733: // damaged, and the caret will stay roughly where it belongs.
0734: List<Integer> offsets = new ArrayList<Integer>();
0735: List<Integer> indents = new ArrayList<Integer>();
0736:
0737: // When we're formatting sections, include whitespace on empty lines; this
0738: // is used during live code template insertions for example. However, when
0739: // wholesale formatting a whole document, leave these lines alone.
0740: boolean indentEmptyLines = (startOffset != 0 || endOffset != doc
0741: .getLength());
0742:
0743: boolean includeEnd = endOffset == doc.getLength()
0744: || indentOnly;
0745:
0746: // TODO - remove initialbalance etc.
0747: computeIndents(doc, initialIndent, initialOffset,
0748: endOffset, info, offsets, indents,
0749: indentEmptyLines, includeEnd, indentOnly);
0750:
0751: try {
0752: doc.atomicLock();
0753:
0754: // Iterate in reverse order such that offsets are not affected by our edits
0755: assert indents.size() == offsets.size();
0756: org.netbeans.editor.Formatter editorFormatter = doc
0757: .getFormatter();
0758: for (int i = indents.size() - 1; i >= 0; i--) {
0759: int indent = indents.get(i);
0760: int lineBegin = offsets.get(i);
0761:
0762: if (lineBegin < lineStart) {
0763: // We're now outside the region that the user wanted reformatting;
0764: // these offsets were computed to get the correct continuation context etc.
0765: // for the formatter
0766: break;
0767: }
0768:
0769: if (lineBegin == lineStart && i > 0) {
0770: // Look at the previous line, and see how it's indented
0771: // in the buffer. If it differs from the computed position,
0772: // offset my computed position (thus, I'm only going to adjust
0773: // the new line position relative to the existing editing.
0774: // This avoids the situation where you're inserting a newline
0775: // in the middle of "incorrectly" indented code (e.g. different
0776: // size than the IDE is using) and the newline position ending
0777: // up "out of sync"
0778: int prevOffset = offsets.get(i - 1);
0779: int prevIndent = indents.get(i - 1);
0780: int actualPrevIndent = LexUtilities
0781: .getLineIndent(doc, prevOffset);
0782: if (actualPrevIndent != prevIndent) {
0783: // For blank lines, indentation may be 0, so don't adjust in that case
0784: if (!(Utilities.isRowEmpty(doc, prevOffset) || Utilities
0785: .isRowWhite(doc, prevOffset))) {
0786: indent = actualPrevIndent
0787: + (indent - prevIndent);
0788: }
0789: }
0790: }
0791:
0792: // Adjust the indent at the given line (specified by offset) to the given indent
0793: int currentIndent = LexUtilities.getLineIndent(doc,
0794: lineBegin);
0795:
0796: if (currentIndent != indent) {
0797: editorFormatter.changeRowIndent(doc, lineBegin,
0798: indent);
0799: }
0800: }
0801:
0802: if (!indentOnly && codeStyle.reformatComments()) {
0803: reformatComments(doc, startOffset, endOffset);
0804: }
0805: } finally {
0806: doc.atomicUnlock();
0807: }
0808: } catch (BadLocationException ble) {
0809: Exceptions.printStackTrace(ble);
0810: }
0811: }
0812:
0813: public void computeIndents(BaseDocument doc, int initialIndent,
0814: int startOffset, int endOffset, CompilationInfo info,
0815: List<Integer> offsets, List<Integer> indents,
0816: boolean indentEmptyLines, boolean includeEnd,
0817: boolean indentOnly) {
0818: // PENDING:
0819: // The reformatting APIs in NetBeans should be lexer based. They are still
0820: // based on the old TokenID apis. Once we get a lexer version, convert this over.
0821: // I just need -something- in place until that is provided.
0822:
0823: try {
0824: // Algorithm:
0825: // Iterate over the range.
0826: // Accumulate a token balance ( {,(,[, and keywords like class, case, etc. increases the balance,
0827: // },),] and "end" decreases it
0828: // If the line starts with an end marker, indent the line to the level AFTER the token
0829: // else indent the line to the level BEFORE the token (the level being the balance * indentationSize)
0830: // Compute the initial balance and indentation level and use that as a "base".
0831: // If the previous line is not "done" (ends with a comma or a binary operator like "+" etc.
0832: // add a "hanging indent" modifier.
0833: // At the end of the day, we're recording a set of line offsets and indents.
0834: // This can be used either to reformat the buffer, or indent a new line.
0835:
0836: // State:
0837: int offset = Utilities.getRowStart(doc, startOffset); // The line's offset
0838: int end = endOffset;
0839:
0840: int indentSize = codeStyle.getIndentSize();
0841: int hangingIndentSize = codeStyle
0842: .getContinuationIndentSize();
0843:
0844: // Pending - apply comment formatting too?
0845:
0846: // XXX Look up RHTML too
0847: //int indentSize = EditorOptions.get(RubyInstallation.RUBY_MIME_TYPE).getSpacesPerTab();
0848: //int hangingIndentSize = indentSize;
0849:
0850: // Build up a set of offsets and indents for lines where I know I need
0851: // to adjust the offset. I will then go back over the document and adjust
0852: // lines that are different from the intended indent. By doing piecemeal
0853: // replacements in the document rather than replacing the whole thing,
0854: // a lot of things will work better: breakpoints and other line annotations
0855: // will be left in place, semantic coloring info will not be temporarily
0856: // damaged, and the caret will stay roughly where it belongs.
0857:
0858: // The token balance at the offset
0859: int balance = 0;
0860: // The bracket balance at the offset ( parens, bracket, brace )
0861: int bracketBalance = 0;
0862: boolean continued = false;
0863: // boolean indentHtml = false;
0864: // if (embeddedJavaScript) {
0865: // indentHtml = codeStyle.indentHtml();
0866: // }
0867:
0868: int originallockCommentIndention = 0;
0869: int adjustedBlockCommentIndention = 0;
0870:
0871: Set<OffsetRange> ranges = new HashSet<OffsetRange>();
0872:
0873: int endIndents;
0874: while ((!includeEnd && offset < end)
0875: || (includeEnd && offset <= end)) {
0876: int indent; // The indentation to be used for the current line
0877:
0878: // No compound indentation for JavaScript
0879: // if (embeddedJavaScript && !indentOnly) {
0880: // // Pick up the indentation level assigned by the HTML indenter; gets HTML structure
0881: // initialIndent = LexUtilities.getLineIndent(doc, offset);
0882: // }
0883:
0884: final int IN_CODE = 0;
0885: final int IN_LITERAL = 1;
0886: final int IN_BLOCK_COMMENT_START = 2;
0887: final int IN_BLOCK_COMMENT_MIDDLE = 3;
0888: int lineType = IN_CODE;
0889: int pos = Utilities.getRowFirstNonWhite(doc, offset);
0890: TokenSequence<? extends JsTokenId> ts = null;
0891:
0892: if (pos != -1) {
0893: // I can't look at the first position on the line, since
0894: // for a string array that is indented, the indentation portion
0895: // is recorded as a blank identifier
0896: ts = LexUtilities.getPositionedSequence(doc, pos);
0897:
0898: if (ts != null) {
0899: TokenId id = ts.token().id();
0900: // We don't have multiline string literals in JavaScript!
0901: if (id == JsTokenId.BLOCK_COMMENT) {
0902: if (ts.offset() == pos) {
0903: lineType = IN_BLOCK_COMMENT_START;
0904: originallockCommentIndention = LexUtilities
0905: .getLineIndent(doc, offset);
0906: } else {
0907: lineType = IN_BLOCK_COMMENT_MIDDLE;
0908: }
0909: } else if (id == JsTokenId.NONUNARY_OP) {
0910: // If a line starts with a non unary operator we can
0911: // assume it's a continuation from a previous line
0912: continued = true;
0913: } else if (id == JsTokenId.STRING_LITERAL
0914: || id == JsTokenId.STRING_END
0915: || id == JsTokenId.REGEXP_LITERAL
0916: || id == JsTokenId.REGEXP_END) {
0917: // You can get multiline literals in JavaScript by inserting a \ at the end
0918: // of the line
0919: lineType = IN_LITERAL;
0920: }
0921: } else {
0922: // No ruby token -- leave the formatting alone!
0923: // (Probably in an RHTML file on a line with no JavaScript)
0924: lineType = IN_LITERAL;
0925: }
0926: }
0927:
0928: int hangingIndent = continued ? (hangingIndentSize) : 0;
0929:
0930: if (lineType == IN_LITERAL) {
0931: // Skip this line - leave formatting as it is prior to reformatting
0932: indent = LexUtilities.getLineIndent(doc, offset);
0933:
0934: // No compound indent for JavaScript
0935: // if (embeddedJavaScript && indentHtml && balance > 0) {
0936: // indent += balance * indentSize;
0937: // }
0938: } else if (lineType == IN_BLOCK_COMMENT_MIDDLE) {
0939: if (doc.getText(pos, 1).charAt(0) == '*') {
0940: // *-lines get indented to be flushed with the * in /*, other lines
0941: // get indented to be aligned with the presumably indented text content!
0942: //indent = LexUtilities.getLineIndent(doc, ts.offset())+1;
0943: indent = adjustedBlockCommentIndention + 1;
0944: } else {
0945: // Leave indentation of comment blocks alone since they probably correspond
0946: // to commented out code - we don't want to lose the indentation.
0947: // Possibly, I could shift the code all relative to the first line
0948: // in the commented out block... A possible later enhancement.
0949: // This shifts by the starting line which is wrong - should use the first comment line
0950: //indent = LexUtilities.getLineIndent(doc, offset)-originallockCommentIndention+adjustedBlockCommentIndention;
0951: indent = LexUtilities
0952: .getLineIndent(doc, offset);
0953: }
0954: } else if ((endIndents = isEndIndent(doc, offset)) > 0) {
0955: indent = (balance - endIndents) * indentSize
0956: + hangingIndent + initialIndent;
0957: } else {
0958: assert lineType == IN_CODE
0959: || lineType == IN_BLOCK_COMMENT_START;
0960: indent = balance * indentSize + hangingIndent
0961: + initialIndent;
0962: if (lineType == IN_BLOCK_COMMENT_START) {
0963: adjustedBlockCommentIndention = indent;
0964: }
0965: }
0966:
0967: if (indent < 0) {
0968: indent = 0;
0969: }
0970:
0971: int lineBegin = Utilities.getRowFirstNonWhite(doc,
0972: offset);
0973:
0974: // Insert whitespace on empty lines too -- needed for abbreviations expansion
0975: if (lineBegin != -1 || indentEmptyLines) {
0976: // Don't do a hanging indent if we're already indenting beyond the parent level?
0977:
0978: indents.add(Integer.valueOf(indent));
0979: offsets.add(Integer.valueOf(offset));
0980: }
0981:
0982: int endOfLine = Utilities.getRowEnd(doc, offset) + 1;
0983:
0984: if (lineBegin != -1) {
0985: balance += getTokenBalance(doc, lineBegin,
0986: endOfLine, true, ranges);
0987: int bracketDelta = getTokenBalance(doc, lineBegin,
0988: endOfLine, false, ranges);
0989: bracketBalance += bracketDelta;
0990: continued = isLineContinued(doc, offset,
0991: bracketBalance);
0992: }
0993:
0994: offset = endOfLine;
0995: }
0996: } catch (BadLocationException ble) {
0997: Exceptions.printStackTrace(ble);
0998: }
0999: }
1000:
1001: void reformatComments(BaseDocument doc, int start, int end) {
1002: int rightMargin = rightMarginOverride;
1003: if (rightMargin == -1) {
1004: CodeStyle style = codeStyle;
1005: if (style == null) {
1006: style = CodeStyle.getDefault(null);
1007: }
1008:
1009: rightMargin = style.getRightMargin();
1010: }
1011:
1012: // ReflowParagraphAction action = new ReflowParagraphAction();
1013: // action.reflowComments(doc, start, end, rightMargin);
1014: throw new RuntimeException("Not yet implemented!");
1015: }
1016:
1017: /**
1018: * Ensure that the editor-settings for tabs match our code style, since the
1019: * primitive "doc.getFormatter().changeRowIndent" calls will be using
1020: * those settings
1021: */
1022: private static void syncOptions(BaseDocument doc, CodeStyle style) {
1023: org.netbeans.editor.Formatter formatter = doc.getFormatter();
1024: if (formatter.getSpacesPerTab() != style.getIndentSize()) {
1025: formatter.setSpacesPerTab(style.getIndentSize());
1026: }
1027: }
1028:
1029: /**
1030: * One item in indent stack, see description of stack variable
1031: */
1032: private static final class StackItem {
1033:
1034: private StackItem(boolean braceless, OffsetRange range) {
1035: this .braceless = braceless;
1036: this .range = range;
1037: }
1038:
1039: /**
1040: * true for block without optional curly braces, false otherwise
1041: */
1042: private final boolean braceless;
1043:
1044: /**
1045: * For braceless blocks it is range from statement beginning (e.g. |if...)
1046: * to end of line where curly brace would be (e.g. if(...) |\n )<br>
1047: * For braces and brackets blocks it is offset of beginning of token for
1048: * both - beginning and end of range (e.g. OffsetRange[ts.token(), ts.token()])
1049: */
1050: private final OffsetRange range;
1051:
1052: public String toString() {
1053: return "StackItem[" + braceless + "," + range + "]";
1054: }
1055: }
1056:
1057: }
|