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): The Original Software is NetBeans. The Initial
0025: * Developer of the Original Software is Sun Microsystems, Inc. Portions
0026: * Copyright 1997-2006 Sun Microsystems, Inc. All Rights Reserved.
0027: *
0028: * If you wish your version of this file to be governed by only the CDDL
0029: * or only the GPL Version 2, indicate your decision by adding
0030: * "[Contributor] elects to include this software in this distribution
0031: * under the [CDDL or GPL Version 2] license." If you do not indicate a
0032: * single choice of license, a recipient has the option to distribute
0033: * your version of this file under either the CDDL, the GPL Version 2 or
0034: * to extend the choice of license to its licensees as provided above.
0035: * However, if you add GPL Version 2 code and therefore, elected the GPL
0036: * Version 2 license, then the option applies only if the new code is
0037: * made subject to such option by the copyright holder.
0038: */
0039:
0040: package org.netbeans.modules.php.editor;
0041:
0042: import javax.swing.text.BadLocationException;
0043: import javax.swing.text.Caret;
0044: import javax.swing.text.Document;
0045:
0046: import org.netbeans.modules.gsf.api.OffsetRange;
0047: import org.netbeans.api.lexer.Token;
0048: import org.netbeans.api.lexer.TokenSequence;
0049: import org.netbeans.api.lexer.TokenUtilities;
0050: import org.netbeans.editor.BaseDocument;
0051: import org.netbeans.editor.Utilities;
0052:
0053: /**
0054: * This static class groups the whole aspect of bracket completion. It is
0055: * defined to clearly separate the functionality and keep actions clean. The
0056: * methods of the class are called from different actions as KeyTyped,
0057: * DeletePreviousChar.
0058: *
0059: * @author ads ( BraceCompletion from org.netbeans.modules.editor.java
0060: * package is resued ).
0061: */
0062: class BracketCompletion {
0063:
0064: public static OffsetRange findMatching(Document doc, int caretOffset) {
0065: // TODO Auto-generated method stub
0066: return null;
0067: }
0068:
0069: /**
0070: * Returns true if bracket completion is enabled in options.
0071: */
0072: static boolean completionSettingEnabled() {
0073: // TODO
0074: /*return ((Boolean) Settings.getValue(PhpKit.class,
0075: ExtSettingsNames.PAIR_CHARACTERS_COMPLETION)).booleanValue();*/
0076: return true;
0077: }
0078:
0079: /**
0080: * Returns true if it is selected in Options
0081: * to add "\".\n\"" on line break
0082: */
0083: static boolean useDotConnectorInStringSetting() {
0084: // TODO
0085: return false;
0086: }
0087:
0088: /**
0089: * A hook method called after a character was inserted into the document.
0090: * The function checks for special characters for completion ()[]'"{} and
0091: * other conditions and optionally performs changes to the doc and or caret
0092: * (complets braces, moves caret, etc.)
0093: *
0094: * @param doc
0095: * the document where the change occurred
0096: * @param dotPos
0097: * position of the character insertion
0098: * @param caret
0099: * caret
0100: * @param ch
0101: * the character that was inserted
0102: * @throws BadLocationException
0103: * if dotPos is not correct
0104: */
0105: static void charInserted(BaseDocument doc, int dotPos, Caret caret,
0106: char ch) throws BadLocationException {
0107: if (!completionSettingEnabled()) {
0108: return;
0109: }
0110: if (ch == ')' || ch == ']' || ch == '(' || ch == '[') {
0111: Token tokenAtDot = TokenUtils.getPhpToken(doc, dotPos);
0112:
0113: // tokenAtDot is inserted char, not the next one.
0114: if (isTokenTextEquals(tokenAtDot, TokenUtils.RBRACKET)
0115: || isTokenTextEquals(tokenAtDot, TokenUtils.RPAREN)) {
0116: skipClosingBracket(doc, caret, ch);
0117: } else if (isTokenTextEquals(tokenAtDot,
0118: TokenUtils.LBRACKET)
0119: || isTokenTextEquals(tokenAtDot, TokenUtils.LPAREN)) {
0120: completeOpeningBracket(doc, dotPos, caret, ch);
0121: }
0122: } else if (ch == ';') {
0123: processSemicolon(doc, dotPos, caret);
0124: }
0125: }
0126:
0127: /**
0128: * Hook called after a character *ch* was backspace-deleted from *doc*. The
0129: * function possibly removes bracket or quote pair if appropriate.
0130: *
0131: * @param doc
0132: * the document
0133: * @param dotPos
0134: * position of the change
0135: * @param caret
0136: * caret
0137: * @param ch
0138: * the character that was deleted
0139: */
0140: static void charBackspaced(BaseDocument doc, int dotPos,
0141: Caret caret, char ch) throws BadLocationException {
0142: if (completionSettingEnabled()) {
0143: if (ch == '(' || ch == '[') {
0144: if (isRemovePairedBracket(doc, dotPos, ch)) {
0145: doc.remove(dotPos, 1);
0146: }
0147: } else if (ch == '\"' || ch == '\'' || ch == '`') {
0148: if (!isEscapeSequence(doc, dotPos)) {
0149: char match[] = doc.getChars(dotPos, 1);
0150: if (match != null && match[0] == ch) {
0151: doc.remove(dotPos, 1);
0152: }
0153: }
0154: }
0155: }
0156: }
0157:
0158: private static boolean isRemovePairedBracket(BaseDocument doc,
0159: int dotPos, char ch) {
0160: Token tokenAtDot = TokenUtils.getPhpToken(doc, dotPos);
0161:
0162: if (TokenUtils.LBRACKET.equals("" + ch)) {
0163: if (isTokenTextEquals(tokenAtDot, TokenUtils.RBRACKET)
0164: && !isBalanced(doc, TokenUtils.LBRACKET,
0165: TokenUtils.RBRACKET)) {
0166: return true;
0167: }
0168: } else if (TokenUtils.LPAREN.equals("" + ch)) {
0169: if (isTokenTextEquals(tokenAtDot, TokenUtils.RPAREN)
0170: && !isBalanced(doc, TokenUtils.LPAREN,
0171: TokenUtils.RPAREN)) {
0172: return true;
0173: }
0174: }
0175: return false;
0176: }
0177:
0178: /**
0179: * Resolve whether pairing right curly should be added automatically at the
0180: * caret position or not. <br>
0181: * There must be only whitespace or line comment or block comment between
0182: * the caret position and the left brace and the left brace must be on the
0183: * same line where the caret is located. <br>
0184: * The caret must not be "contained" in the opened block comment token.
0185: *
0186: * @param doc
0187: * document in which to operate.
0188: * @param caretOffset
0189: * offset of the caret.
0190: * @return true if a right brace '}' should be added or false if not.
0191: */
0192: static boolean isAddRightBrace(BaseDocument doc, int caretOffset)
0193: throws BadLocationException {
0194: boolean addRightBrace = false;
0195: if (completionSettingEnabled() && caretOffset > 0) {
0196:
0197: TokenSequence sequence = TokenUtils.getTokenSequence(doc);
0198: int tokenOffset = TokenUtils.getTokenOffset(sequence,
0199: caretOffset);
0200:
0201: // suppose that right brace should be added
0202: addRightBrace = true;
0203:
0204: /*
0205: * Disable right brace adding if caret not positioned within
0206: * whitespace or line comment
0207: */
0208: addRightBrace = caretInsideAllowedTokens(sequence,
0209: caretOffset, tokenOffset);
0210:
0211: if (addRightBrace) {// still candidate for adding
0212: /*
0213: * Check whether there are only whitespace or comment tokens
0214: * between caret and left brace and check only on the line
0215: * with the caret
0216: */
0217: addRightBrace = checkAllowedTokensOnTheLeft(doc,
0218: sequence, caretOffset, tokenOffset);
0219: }
0220:
0221: if (addRightBrace) {
0222: /*
0223: * Finally check the brace balance
0224: * whether there are any missing right
0225: * braces
0226: */
0227: addRightBrace = (braceBalance(doc) > 0);
0228: }
0229: }
0230: return addRightBrace;
0231: }
0232:
0233: static void addRightBrace(BaseDocument doc, Caret caret, int dotPos)
0234: throws BadLocationException {
0235: int end = getRowOrBlockEnd(doc, dotPos);
0236: doc.insertString(end, "}", null); // NOI18N
0237: caret.setDot(dotPos);
0238: }
0239:
0240: /**
0241: * Returns position of the first unpaired closing paren/brace/bracket from
0242: * the caretOffset till the end of caret row. If there is no such element,
0243: * position after the last non-white character on the caret row is returned.
0244: */
0245: static int getRowOrBlockEnd(BaseDocument doc, int caretOffset)
0246: throws BadLocationException {
0247: int rowEnd = Utilities.getRowLastNonWhite(doc, caretOffset);
0248: if (rowEnd == -1 || caretOffset >= rowEnd) {
0249: return caretOffset;
0250: }
0251: rowEnd += 1;
0252: int parenBalance = 0;
0253: int braceBalance = 0;
0254: int bracketBalance = 0;
0255: TokenSequence sequence = TokenUtils.getTokenSequence(doc)
0256: .subSequence(caretOffset, rowEnd);
0257: while (sequence.moveNext() && sequence.offset() < rowEnd) {
0258: Token token = sequence.token();
0259: if (isTokenTextEquals(token, TokenUtils.LPAREN)) {
0260: parenBalance++;
0261: } else if (isTokenTextEquals(token, TokenUtils.RPAREN)) {
0262: if (parenBalance-- == 0) {
0263: return sequence.offset();
0264: }
0265: } else if (isTokenTextEquals(token, TokenUtils.LBRACE)) {
0266: braceBalance++;
0267: } else if (isTokenTextEquals(token, TokenUtils.RBRACE)) {
0268: if (braceBalance-- == 0) {
0269: return sequence.offset();
0270: }
0271: } else if (isTokenTextEquals(token, TokenUtils.LBRACKET)) {
0272: bracketBalance++;
0273: } else if (isTokenTextEquals(token, TokenUtils.RBRACKET)) {
0274: if (bracketBalance-- == 0) {
0275: return sequence.offset();
0276: }
0277:
0278: }
0279: }
0280: return rowEnd;
0281: }
0282:
0283: /**
0284: * Check whether the typed bracket should stay in the document or be
0285: * removed. <br>
0286: * This method is called by <code>skipClosingBracket()</code>.
0287: *
0288: * @param doc
0289: * document into which typing was done.
0290: * @param caretOffset
0291: * position of the caret in document
0292: * @param tokenText
0293: * inserted bracket token text
0294: * @param leftBracket
0295: * paired left bracket token text
0296: */
0297: static boolean isSkipClosingBracket(BaseDocument doc,
0298: int caretOffset, String bracketText, String leftBracketText)
0299: throws BadLocationException {
0300: // First check whether the caret is not after the last char in the document
0301: // because no bracket would follow then so it could not be skipped.
0302: if (caretOffset == doc.getLength()) {
0303: return false; // no skip in this case
0304: }
0305:
0306: boolean skipClosingBracket = false; // by default do not remove
0307: TokenSequence sequence = TokenUtils.getTokenSequence(doc);
0308: Token token = TokenUtils.getToken(sequence, caretOffset);
0309:
0310: if (token == null || !isTokenTextEquals(token, bracketText)) {
0311: return skipClosingBracket;
0312: }
0313:
0314: int balance = balanceBetweenBraces(doc, caretOffset,
0315: leftBracketText, bracketText);
0316: if (balance <= 0) {
0317: // this means amount of bracketText is more or equals to
0318: // amount of leftBracketText.
0319: skipClosingBracket = true;
0320: }
0321: return skipClosingBracket;
0322: }
0323:
0324: /**
0325: * Check for conditions and possibly complete an already inserted quote .
0326: *
0327: * @param doc
0328: * the document
0329: * @param dotPos
0330: * position of the opening bracket (already in the doc)
0331: * @param caret
0332: * caret
0333: * @param bracket
0334: * the character that was inserted
0335: */
0336: static boolean completeQuote(BaseDocument doc, int dotPos,
0337: Caret caret, char bracket) throws BadLocationException {
0338:
0339: if (!completionSettingEnabled()) {
0340: return false;
0341: }
0342:
0343: if (isEscapeSequence(doc, dotPos)) { // \" or \' typed
0344: return false;
0345: }
0346:
0347: Token tokenAtDot = TokenUtils.getPhpToken(doc, dotPos);
0348:
0349: // dotPos is inside comment
0350: if (isCommentToken(tokenAtDot)) {
0351: return false;
0352: }
0353:
0354: if (isEODStringToken(tokenAtDot)) {
0355: return false;
0356: }
0357: // dotPos is inside String
0358: boolean insideString = isStringToken(tokenAtDot);
0359: boolean unpairedQuoteToken = hasUnpairedQuoteToken(doc, bracket);
0360:
0361: if (insideString) {
0362: if (unpairedQuoteToken
0363: && retypedEnclosingBracket(doc, dotPos, bracket)) {
0364: return true;
0365: }
0366: }
0367:
0368: // if quotes are not still balanced and we are not inside string,
0369: // will add paired quote.
0370: if (!insideString && unpairedQuoteToken) {
0371: String toInsert = "" + bracket + bracket;
0372:
0373: if (isSemicolonExpected(doc, dotPos)) {
0374: toInsert += TokenUtils.SEMICOLON;
0375: } else if (isColonExpected(doc, dotPos)) {
0376: toInsert += TokenUtils.COLON;
0377: }
0378: doc.insertString(dotPos, toInsert, null); // NOI18N
0379: return true;
0380: }
0381: return false;
0382: }
0383:
0384: /**
0385: * returns true if php_string token is placed
0386: * on dotPos offset in doc document
0387: */
0388: static boolean posWithinString(BaseDocument doc, int dotPos) {
0389: Token token = TokenUtils.getPhpToken(doc, dotPos);
0390: return isStringToken(token);
0391: }
0392:
0393: /**
0394: * returns true if there is ''
0395: */
0396: static boolean isNotClosedEOD(BaseDocument doc, int dotPos) {
0397: try {
0398: TokenSequence sequence = getTokenSequenceForRow(doc, dotPos);
0399: Token token = TokenUtils.getToken(sequence, dotPos);
0400: Token prevToken = getFirstNonWhiteTokenBwd(sequence, dotPos);
0401:
0402: if (prevToken == null || token == null) {
0403: return false;
0404: }
0405:
0406: if (isEODStringToken(token)) {
0407: return false;
0408: }
0409:
0410: if (TokenUtils.getTokenType(prevToken).equals(
0411: TokenUtils.EOD_OPERATOR)) {
0412: return true;
0413: }
0414: } catch (BadLocationException ex) {
0415: }
0416: return false;
0417: }
0418:
0419: static String getEODStringLabel(BaseDocument doc, int dotPos) {
0420: try {
0421: int firstNonWhite = TokenUtils.getTokenOffset(doc, dotPos);
0422: int lastNonWhite = Utilities.getFirstNonWhiteBwd(doc,
0423: dotPos);
0424: if (firstNonWhite >= 0 && lastNonWhite >= firstNonWhite) {
0425: return doc.getText(firstNonWhite, lastNonWhite
0426: - firstNonWhite + 1);
0427: }
0428: } catch (BadLocationException ex) {
0429: }
0430: return "";
0431: }
0432:
0433: private static boolean isColonExpected(BaseDocument doc, int dotPos)
0434: throws BadLocationException {
0435: Token nextToken = getFirstNonWhiteTokenFwd(doc, dotPos);
0436: Token prevToken = getFirstNonWhiteTokenBwd(doc, dotPos);
0437: if (isEndOfLine(doc, dotPos)) {
0438: if (nextToken == null || prevToken == null) {
0439: return false;
0440: }
0441: if (isTokenTextEquals(prevToken, TokenUtils.CASE)
0442: && !isTokenTextEquals(nextToken, TokenUtils.COLON)) {
0443: return true;
0444: }
0445: }
0446: return false;
0447: }
0448:
0449: /**
0450: * if cursor is on the end of line and there are no
0451: * ), ], . or ; symbols after it, add ; after added paired quote.
0452: *
0453: */
0454: private static boolean isSemicolonExpected(BaseDocument doc,
0455: int dotPos) throws BadLocationException {
0456: Token nextToken = getFirstNonWhiteTokenFwd(doc, dotPos);
0457: Token prevToken = getFirstNonWhiteTokenBwd(doc, dotPos);
0458: if (!isEndOfLine(doc, dotPos)) {
0459: return false;
0460: }
0461: // next is allowed to be null
0462: boolean nextIncorrect = (nextToken != null && (isStringToken(nextToken)
0463: || isTokenTextEquals(nextToken, TokenUtils.DOT)
0464: || isTokenTextEquals(nextToken, TokenUtils.SEMICOLON)
0465: || isTokenTextEquals(nextToken, TokenUtils.RBRACKET)
0466: || isTokenTextEquals(nextToken, TokenUtils.RPAREN) || isTokenTextEquals(
0467: nextToken, TokenUtils.COLON)));
0468: // next is NOT allowed to be null
0469: boolean prevIncorrect = (prevToken != null && (isTokenTextEquals(
0470: prevToken, TokenUtils.CASE)
0471: || isTokenTextEquals(prevToken,
0472: TokenUtils.ARRAY_PAIR_MAPPER)
0473: || isTokenTextEquals(prevToken, TokenUtils.COMMA) || isTokenTextEquals(
0474: prevToken, TokenUtils.LPAREN)));
0475:
0476: if (nextIncorrect || prevIncorrect) {
0477: return false;
0478: }
0479:
0480: return true;
0481: }
0482:
0483: /**
0484: * returns token sequence retrieved from full document's tokenSequence
0485: * using subSequence(rowOffset, dotPos);
0486: * rowOffset was calculated using Utilities.getRowStart(doc, dotPos)
0487: */
0488: private static TokenSequence getTokenSequenceForRow(
0489: BaseDocument doc, int dotPos) throws BadLocationException {
0490: TokenSequence seq = TokenUtils.getTokenSequence(doc);
0491: int rowOffset = Utilities.getRowStart(doc, dotPos);
0492: return seq.subSequence(rowOffset, dotPos);
0493: }
0494:
0495: private static Token getFirstNonWhiteTokenFwd(BaseDocument doc,
0496: int dotPos) throws BadLocationException {
0497: TokenSequence sequence = TokenUtils.getTokenSequence(doc);
0498: return getFirstNonWhiteTokenFwd(sequence, dotPos);
0499: }
0500:
0501: private static Token getFirstNonWhiteTokenFwd(
0502: TokenSequence sequence, int dotPos)
0503: throws BadLocationException {
0504: sequence.move(dotPos);
0505: while (sequence.moveNext()) {
0506: Token token = sequence.token();
0507: if (!TokenUtils.getTokenType(token).equals(
0508: TokenUtils.WHITESPACE)) {
0509: return sequence.token();
0510: }
0511: }
0512: return null;
0513: }
0514:
0515: private static Token getFirstNonWhiteTokenBwd(BaseDocument doc,
0516: int dotPos) throws BadLocationException {
0517: TokenSequence sequence = TokenUtils.getTokenSequence(doc);
0518: return getFirstNonWhiteTokenBwd(sequence, dotPos);
0519: }
0520:
0521: private static Token getFirstNonWhiteTokenBwd(
0522: TokenSequence sequence, int dotPos)
0523: throws BadLocationException {
0524: sequence.move(dotPos);
0525: while (sequence.movePrevious()) {
0526: Token token = sequence.token();
0527: if (!TokenUtils.getTokenType(token).equals(
0528: TokenUtils.WHITESPACE)) {
0529: return sequence.token();
0530: }
0531: }
0532: return null;
0533: }
0534:
0535: /**
0536: * Counts the number of braces starting at dotPos to the end of the
0537: * document. Every occurence of { increses the count by 1, every occurrence
0538: * of } decreses the count by 1. The result is returned.
0539: *
0540: * @return The number of { - number of } (>0 more { than } ,<0 more } than {)
0541: */
0542: private static int braceBalance(BaseDocument doc)
0543: throws BadLocationException {
0544: return tokenBalance(doc, TokenUtils.LBRACE, TokenUtils.RBRACE);
0545: }
0546:
0547: /**
0548: * The same as braceBalance but generalized to any pair of matching tokens.
0549: *
0550: * @param open
0551: * the token that increses the count
0552: * @param close
0553: * the token that decreses the count
0554: */
0555: private static int tokenBalance(BaseDocument doc, String open,
0556: String close) throws BadLocationException {
0557: BalanceTokenProcessor processor = new BalanceTokenProcessor(
0558: open, close);
0559: return processor.checkBalance(doc);
0560: }
0561:
0562: private static int tokenBalance(BaseDocument doc, String open,
0563: String close, int startOffset, int endOffset)
0564: throws BadLocationException {
0565: BalanceTokenProcessor processor = new BalanceTokenProcessor(
0566: open, close);
0567: return processor.checkBalance(doc, startOffset, endOffset);
0568: }
0569:
0570: /**
0571: * A hook to be called after closing bracket ) or ] was inserted into the
0572: * document. The method checks if the bracket should stay there or be
0573: * removed and some exisitng bracket just skipped.
0574: *
0575: * @param doc
0576: * the document
0577: * @param dotPos
0578: * position of the inserted bracket
0579: * @param caret
0580: * caret
0581: * @param bracket
0582: * the bracket character ']' or ')'
0583: */
0584: private static void skipClosingBracket(BaseDocument doc,
0585: Caret caret, char bracket) throws BadLocationException {
0586:
0587: String bracketText = (bracket == ')') ? TokenUtils.RPAREN
0588: : TokenUtils.RBRACKET;
0589: String left = (bracket == ')') ? TokenUtils.LPAREN
0590: : TokenUtils.LBRACKET;
0591:
0592: int caretOffset = caret.getDot();
0593: if (isSkipClosingBracket(doc, caretOffset, bracketText, left)) {
0594: doc.remove(caretOffset - 1, 1);
0595: caret.setDot(caretOffset); // skip closing bracket
0596: }
0597: }
0598:
0599: /**
0600: * finds { or } brace on the left. The same on the right.
0601: * Checks balance of leftTokenText and rightTokenText in this period.
0602: * @returns number or leftTokenText occurances minus rightTokenText occurances
0603: */
0604: private static int balanceBetweenBraces(BaseDocument doc,
0605: int caretOffset, String leftTokenText, String rightTokenText)
0606: throws BadLocationException {
0607: int prevBraceOffset = findFirstTokenBwd(doc, caretOffset,
0608: TokenUtils.LBRACE, TokenUtils.RBRACE);
0609: int nextBraceOffset = findFirstTokenFwd(doc, caretOffset,
0610: TokenUtils.LBRACE, TokenUtils.RBRACE);
0611:
0612: // token balance
0613: int balance = tokenBalance(doc, leftTokenText, rightTokenText,
0614: prevBraceOffset, nextBraceOffset);
0615: return balance;
0616: }
0617:
0618: /**
0619: * Searches for the first appearance of any token text specified in tokenText.
0620: * Looks in forward direction (increasing offset).
0621: * Starts from token placed on caretOffset offsed.
0622: */
0623: private static int findFirstTokenFwd(BaseDocument doc,
0624: int caretOffset, CharSequence... tokenText) {
0625: TokenSequence sequence = TokenUtils.getTokenSequence(doc);
0626: sequence.move(caretOffset);
0627: int offset = -1;
0628: while (sequence.moveNext()) {
0629:
0630: if (isTokenTextEquals(sequence.token(), tokenText)) {
0631: offset = sequence.offset();
0632: break;
0633: }
0634: }
0635: return offset;
0636: }
0637:
0638: /**
0639: * Searches for the first appearance of any token text specified in tokenText.
0640: * Looks in backward direction (decreasing offset).
0641: * Starts from token placed on caretOffset offsed.
0642: */
0643: private static int findFirstTokenBwd(BaseDocument doc,
0644: int caretOffset, CharSequence... tokenText) {
0645: TokenSequence sequence = TokenUtils.getTokenSequence(doc);
0646: sequence.move(caretOffset);
0647: int offset = -1;
0648: while (sequence.movePrevious()) {
0649: if (isTokenTextEquals(sequence.token(), tokenText)) {
0650: offset = sequence.offset();
0651: break;
0652: }
0653: }
0654: return offset;
0655: }
0656:
0657: /**
0658: * checks if tokenText is equals to any element specified in toCompare.
0659: */
0660: private static boolean isTokenTextEquals(Token token,
0661: CharSequence... toCompare) {
0662: boolean result = false;
0663: if (token != null) {
0664: for (CharSequence test : toCompare) {
0665: if (TokenUtilities.textEquals(token.text(), test)) {
0666: result = true;
0667: break;
0668: }
0669: }
0670: }
0671: return result;
0672:
0673: }
0674:
0675: /**
0676: * Check for various conditions and possibly add a pairing bracket to the
0677: * already inserted.
0678: *
0679: * @param doc
0680: * the document
0681: * @param dotPos
0682: * position of the opening bracket (already in the doc)
0683: * @param caret
0684: * caret
0685: * @param bracket
0686: * the bracket that was inserted
0687: */
0688: private static void completeOpeningBracket(BaseDocument doc,
0689: int dotPos, Caret caret, char bracket)
0690: throws BadLocationException {
0691: String leftBracket = "" + bracket;
0692: String rightBracket = "" + matching(bracket);
0693:
0694: int balance = balanceBetweenBraces(doc, dotPos, leftBracket,
0695: rightBracket);
0696: if (isCompletablePosition(doc, dotPos + 1) && balance > 0) {
0697: doc.insertString(dotPos + 1, rightBracket, null);
0698: caret.setDot(dotPos + 1);
0699: }
0700: }
0701:
0702: private static boolean isEscapeSequence(BaseDocument doc, int dotPos)
0703: throws BadLocationException {
0704: if (dotPos <= 0)
0705: return false;
0706: char previousChar = doc.getChars(dotPos - 1, 1)[0];
0707: return previousChar == '\\';
0708: }
0709:
0710: /**
0711: * Checks if caret is positioned within whitespace or line comment.
0712: * @return false if caret is inside any token except WHITESPACE or LINE_COMMENT.
0713: * Otherwise true.
0714: */
0715: private static boolean caretInsideAllowedTokens(
0716: TokenSequence sequence, int caretOffset, int tokenOffset) {
0717: Token tokenX = TokenUtils.getToken(sequence, caretOffset);
0718: int off = caretOffset - tokenOffset;
0719: if (off > 0 && off < tokenX.length()) {
0720: // caret contained in token
0721: // these tokens are OK
0722: return TokenUtils.getTokenType(tokenX).equals(
0723: TokenUtils.WHITESPACE)
0724: || TokenUtils.getTokenType(tokenX).equals(
0725: TokenUtils.LINE_COMMENT);
0726: }
0727: // caret is not inside token
0728: return true;
0729: }
0730:
0731: /**
0732: * Check whether there are only whitespace or comment tokens
0733: * between caret and left brace and check only on the line
0734: * with the caret
0735: */
0736: private static boolean checkAllowedTokensOnTheLeft(
0737: BaseDocument doc, TokenSequence sequence, int caretOffset,
0738: int tokenOffset) {
0739: boolean isAllowed = false;
0740: try {
0741: sequence.move(tokenOffset);
0742: int caretRowStartOffset = Utilities.getRowStart(doc,
0743: caretOffset);
0744:
0745: while (sequence.movePrevious()
0746: && sequence.offset() >= caretRowStartOffset) {
0747: Token token = sequence.token();
0748: // Assuming java token context here
0749: boolean allowedTokens = TokenUtils.getTokenType(token)
0750: .equals(TokenUtils.WHITESPACE)
0751: || isCommentToken(token);
0752:
0753: if (allowedTokens) {
0754: continue;
0755: }
0756: /* the only success case is when we meet LBRACE before we
0757: meet any another token
0758: */
0759: if (isTokenTextEquals(token, TokenUtils.LBRACE)) {
0760: isAllowed = true;
0761: }
0762: break;
0763: }
0764: } catch (BadLocationException ex) {
0765: }
0766: /* we are here means:
0767: - we have got exception
0768: - achieved line beginning
0769: - have no tokens on the left
0770: - have met not allowed token which was not LBRACE
0771: */
0772: return isAllowed;
0773: }
0774:
0775: private static boolean isEndOfLine(BaseDocument doc, int dotPos)
0776: throws BadLocationException {
0777: int lastNonWhite = Utilities.getRowLastNonWhite(doc, dotPos);
0778: // eol - true if the caret is at the end of line (ignoring whitespaces)
0779: boolean eol = lastNonWhite < dotPos;
0780: return eol;
0781: }
0782:
0783: /**
0784: * if we retype the first or the last quote of the string,
0785: * overwrites existing quote and returns true.
0786: * Otherwise returns false.
0787: */
0788: // fix for #69524
0789: private static boolean retypedEnclosingBracket(BaseDocument doc,
0790: int dotPos, char bracket) throws BadLocationException {
0791: char chr = doc.getChars(dotPos, 1)[0];
0792: if (chr == bracket) {
0793: TokenSequence seq = TokenUtils.getTokenSequence(doc);
0794: int tokenStart = TokenUtils.getTokenOffset(seq, dotPos);
0795: int tokenLength = TokenUtils.getToken(seq, dotPos).length();
0796: int tokenEnd = tokenStart + tokenLength - 1;
0797:
0798: if (dotPos == tokenStart || dotPos == tokenEnd) {
0799: doc.insertString(dotPos, "" + bracket, null); // NOI18N
0800: doc.remove(dotPos, 1);
0801: return true;
0802: }
0803: }
0804: return false;
0805: }
0806:
0807: private static boolean hasUnpairedQuoteToken(BaseDocument doc,
0808: char bracket) {
0809: int count = 0;
0810: String quote = "" + bracket;
0811: TokenSequence sequence = TokenUtils.getTokenSequence(doc);
0812: sequence.moveStart();
0813: while (sequence.moveNext()) {
0814: CharSequence tokenText = sequence.token().text();
0815: if (TokenUtilities.textEquals(tokenText, quote)) {
0816: count++;
0817: }
0818: }
0819: return ((count % 2) == 0);
0820: }
0821:
0822: private static boolean isCommentToken(Token token) {
0823: return (token != null && (TokenUtils.getTokenType(token)
0824: .equals(TokenUtils.BLOCK_COMMENT) || TokenUtils
0825: .getTokenType(token).equals(TokenUtils.LINE_COMMENT)));
0826: }
0827:
0828: private static boolean isStringToken(Token token) {
0829: return (token != null && (TokenUtils.getTokenType(token)
0830: .equals(TokenUtils.STRING)));
0831: }
0832:
0833: private static boolean isEODStringToken(Token token) {
0834: return (token != null && (TokenUtils.getTokenType(token)
0835: .equals(TokenUtils.EOD_STRING)));
0836: }
0837:
0838: /**
0839: * Checks whether dotPos is a position at which bracket and quote completion
0840: * is performed. Brackets and quotes are not completed everywhere but just
0841: * at suitable places .
0842: *
0843: * @param doc
0844: * the document
0845: * @param dotPos
0846: * position to be tested
0847: */
0848: private static boolean isCompletablePosition(BaseDocument doc,
0849: int dotPos) throws BadLocationException {
0850: if (dotPos == doc.getLength()) // there's no other character to test
0851: return true;
0852: else {
0853: // test that we are in front of ) , " or '
0854: char chr = doc.getChars(dotPos, 1)[0];
0855: return (chr == ')' || chr == ',' || chr == '\"'
0856: || chr == '\'' || chr == ' ' || chr == ']'
0857: || chr == '}' || chr == '{' || chr == '\n'
0858: || chr == '\t' || chr == ';');
0859: }
0860: /*
0861: * have adde { in addition to suggested by java editor logic.
0862: * to support case -> if (|{}.
0863: * java will not add paired ) in this case.
0864: */
0865: }
0866:
0867: /**
0868: * Precesses new typed semicolon (;).
0869: * @param doc
0870: * the document
0871: * @param dotPos
0872: * int position on which new ; was added
0873: * @param caret
0874: * Caret. is on the new position, after new typed ;
0875: */
0876: private static void processSemicolon(BaseDocument doc, int dotPos,
0877: Caret caret) throws BadLocationException {
0878: if (isRetypedSemicolon(doc, dotPos, caret)) {
0879: return;
0880: }
0881: moveSemicolon(doc, dotPos, caret);
0882: }
0883:
0884: /**
0885: * processes case of retyped ; at the end of line.
0886: * if ; already existed and was the last token on the line,
0887: * removes newly adde ;, to have only one of them.
0888: * @param doc
0889: * the document
0890: * @param dotPos
0891: * int position on which new ; was added
0892: * @param caret
0893: * Caret. is on the new position, after new typed ;
0894: */
0895: private static boolean isRetypedSemicolon(BaseDocument doc,
0896: int dotPos, Caret caret) throws BadLocationException {
0897:
0898: int caretPos = caret.getDot();
0899: // check if symbol on the caret is the last in the line
0900: if (!isEndOfLine(doc, caretPos + 1)) {
0901: return false;
0902: }
0903:
0904: Token nextToken = TokenUtils.getPhpToken(doc, dotPos + 1);
0905:
0906: if (isTokenTextEquals(nextToken, TokenUtils.SEMICOLON)) {
0907: // remove new semicolon
0908: doc.remove(dotPos, 1);
0909: // set caret pos after semicolon
0910: caret.setDot(caretPos);
0911: return true;
0912: }
0913: return false;
0914: }
0915:
0916: private static void moveSemicolon(BaseDocument doc, int dotPos,
0917: Caret caret) throws BadLocationException {
0918:
0919: int lastParenPos = findLastRParenWithoutOthers(doc, dotPos);
0920: if (lastParenPos < 0) {
0921: return;
0922: }
0923:
0924: Token tokenAfterSColon = TokenUtils.getPhpToken(doc, dotPos);
0925: if (isForLoopSemicolon(doc, dotPos)
0926: || isStringToken(tokenAfterSColon)
0927: || isEODStringToken(tokenAfterSColon)) {
0928: return;
0929: }
0930:
0931: doc.remove(dotPos, 1);
0932: doc.insertString(lastParenPos, ";", null); // NOI18N
0933: caret.setDot(lastParenPos + 1);
0934: }
0935:
0936: /**
0937: * looks for the last right Parenthesis in current line.
0938: * Starts from dotPos offset and goes in forward direction.
0939: * <br/>
0940: * Doesn't permit any symbols other than
0941: * TokenUtils.RPAREN or TokenUtils.WHITESPACE.
0942: * @return last right paren position in current row. returns -1 If there is any symbol
0943: * other than TokenUtils.RPAREN or TokenUtils.WHITESPACE.
0944: *
0945: * <br/>
0946: * is invoked from moveSemicolon( BaseDocument, int, Caret )
0947: */
0948: private static int findLastRParenWithoutOthers(BaseDocument doc,
0949: int dotPos) throws BadLocationException {
0950: int lastParenPos = -1;
0951: int eolPos = Utilities.getRowEnd(doc, dotPos);
0952: TokenSequence sequence = TokenUtils.getTokenSequence(doc)
0953: .subSequence(dotPos, eolPos);
0954: if (sequence == null) {
0955: return -1;
0956: }
0957:
0958: sequence.moveNext(); // skip current ;
0959: while (sequence.moveNext()) {
0960: Token token = sequence.token();
0961: if (isTokenTextEquals(token, TokenUtils.RPAREN)) {
0962: lastParenPos = sequence.offset();
0963: } else if (!isTokenTextEquals(token, TokenUtils.WHITESPACE)) {
0964: return -1;
0965: }
0966: }
0967:
0968: return lastParenPos;
0969: }
0970:
0971: /*
0972: * logic was taken from java editor
0973: */
0974: private static boolean isForLoopSemicolon(BaseDocument doc,
0975: int dotPos) {
0976: TokenSequence sequence = TokenUtils.getTokenSequence(doc);
0977: Token token = TokenUtils.getToken(sequence, dotPos);
0978:
0979: if (!isTokenTextEquals(token, TokenUtils.SEMICOLON)) {
0980: return false;
0981: }
0982:
0983: sequence.move(dotPos);
0984:
0985: int parDepth = 0; // parenthesis depth
0986: int braceDepth = 0; // brace depth
0987: boolean semicolonFound = false; // next semicolon
0988:
0989: while (sequence.movePrevious()) {
0990:
0991: token = sequence.token();
0992: if (isTokenTextEquals(token, TokenUtils.LPAREN)) {
0993: if (parDepth == 0) { // could be a 'for ('
0994: do {
0995: sequence.movePrevious();
0996: token = sequence.token();
0997: } while (isCommentToken(token)
0998: || TokenUtils.getTokenType(token).equals(
0999: TokenUtils.WHITESPACE));
1000:
1001: if (isTokenTextEquals(token, TokenUtils.FOR)) {
1002: return true;
1003: }
1004: return false;
1005: } else { // non-zero depth
1006: parDepth--;
1007: }
1008: } else if (isTokenTextEquals(token, TokenUtils.RPAREN)) {
1009: parDepth++;
1010: } else if (isTokenTextEquals(token, TokenUtils.LBRACE)) {
1011: if (braceDepth == 0) { // unclosed left brace
1012: return false;
1013: }
1014: braceDepth--;
1015: } else if (isTokenTextEquals(token, TokenUtils.RBRACE)) {
1016: braceDepth++;
1017:
1018: } else if (isTokenTextEquals(token, TokenUtils.SEMICOLON)) {
1019: if (semicolonFound) { // one semicolon already found
1020: return false;
1021: }
1022: semicolonFound = true;
1023: }
1024: }
1025: return false;
1026: }
1027:
1028: private static boolean isBalanced(BaseDocument doc, String left,
1029: String right) {
1030: try {
1031: return tokenBalance(doc, left, right) == 0;
1032: } catch (BadLocationException ex) {
1033: return false;
1034: }
1035: }
1036:
1037: /**
1038: * Returns for an opening bracket or quote the appropriate closing
1039: * character.
1040: */
1041: private static char matching(char bracket) {
1042: switch (bracket) {
1043: case '(':
1044: return ')';
1045: case '[':
1046: return ']';
1047: case '\"':
1048: return '\"'; // NOI18N
1049: case '\'':
1050: return '\'';
1051: case '`':
1052: return '`';
1053: default:
1054: return ' ';
1055: }
1056: }
1057:
1058: /**
1059: * Counts the number of specified left and right paired tokens
1060: * Every occurence of leftToken increses the count by 1, every
1061: * occurrence of rightToken decreses the count by 1.
1062: */
1063: private static class BalanceTokenProcessor {
1064:
1065: private static final int BALANCED = 0;
1066:
1067: public BalanceTokenProcessor(String leftToken, String rightToken) {
1068: myLeft = leftToken;
1069: myRight = rightToken;
1070: }
1071:
1072: /**
1073: * @return The number of leftToken minus number of rightToken occurances
1074: */
1075: public int getBalance() {
1076: return myBalance;
1077: }
1078:
1079: /**
1080: * @return true if number of leftToken and rightToken are equal
1081: */
1082: public boolean isBalanced() {
1083: return (getBalance() == BALANCED);
1084: }
1085:
1086: /**
1087: * @returns result of getBalance()
1088: * @see getBalance()
1089: */
1090: public int checkBalance(BaseDocument doc) {
1091: return checkBalance(doc, -1, -1);
1092: }
1093:
1094: /**
1095: * @returns result of getBalance()
1096: * @see getBalance()
1097: */
1098: public int checkBalance(BaseDocument doc, int startOffset,
1099: int endOffset) {
1100: refresh();
1101: TokenSequence sequence = getToketSequence(doc, startOffset,
1102: endOffset);
1103: sequence.moveStart();
1104: while (sequence.moveNext()) {
1105: checkToken(sequence.token());
1106: }
1107: return getBalance();
1108: }
1109:
1110: private TokenSequence getToketSequence(BaseDocument doc,
1111: int startOffset, int endOffset) {
1112: TokenSequence sequence = TokenUtils.getTokenSequence(doc);
1113: if (startOffset < 0 && endOffset < 0) {
1114: return sequence;
1115: }
1116: // if one of offsets was specified ( >=0)
1117: if (startOffset < 0) {
1118: startOffset = 0;
1119: }
1120: if (endOffset < 0) {
1121: endOffset = doc.getLength();
1122: }
1123: return sequence.subSequence(startOffset, endOffset);
1124: }
1125:
1126: private void checkToken(Token token) {
1127: CharSequence tokenText = token.text();
1128: if (TokenUtilities.textEquals(tokenText, myLeft)) {
1129: myBalance++;
1130: } else if (TokenUtilities.textEquals(tokenText, myRight)) {
1131: myBalance--;
1132: }
1133: }
1134:
1135: private void refresh() {
1136: myBalance = 0;
1137: }
1138:
1139: private String myLeft;
1140: private String myRight;
1141:
1142: private int myBalance = 0;
1143: }
1144:
1145: }
|