| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.netbeans.modules.javascript2.editor; |
| |
| import java.util.concurrent.atomic.AtomicReference; |
| import javax.swing.text.BadLocationException; |
| import javax.swing.text.Caret; |
| import org.netbeans.api.editor.mimelookup.MimeRegistration; |
| import org.netbeans.api.lexer.Language; |
| import org.netbeans.api.lexer.Token; |
| import org.netbeans.api.lexer.TokenHierarchy; |
| import org.netbeans.api.lexer.TokenId; |
| import org.netbeans.api.lexer.TokenSequence; |
| import org.netbeans.editor.BaseDocument; |
| import org.netbeans.editor.Utilities; |
| import org.netbeans.modules.csl.api.EditorOptions; |
| import org.netbeans.modules.csl.api.OffsetRange; |
| import org.netbeans.modules.csl.spi.GsfUtilities; |
| import org.netbeans.modules.javascript2.lexer.api.JsTokenId; |
| import org.netbeans.modules.javascript2.lexer.api.LexUtilities; |
| import org.netbeans.modules.javascript2.editor.options.OptionsUtils; |
| import org.netbeans.spi.editor.typinghooks.TypedTextInterceptor; |
| |
| /** |
| * |
| * @author Petr Hejl |
| */ |
| public class JsTypedTextInterceptor implements TypedTextInterceptor { |
| |
| /** Tokens which indicate that we're within a regexp string */ |
| // XXX What about JsTokenId.REGEXP_BEGIN? |
| private static final TokenId[] REGEXP_TOKENS = { JsTokenId.REGEXP, JsTokenId.REGEXP_END }; |
| |
| /** Tokens which indicate that we're within a literal string */ |
| private final static TokenId[] STRING_TOKENS = { JsTokenId.STRING, JsTokenId.STRING_END }; |
| |
| /** Tokens which indicate that we're within a template string */ |
| private final static TokenId[] TEMPLATE_TOKENS = { JsTokenId.TEMPLATE, JsTokenId.TEMPLATE_END }; |
| |
| private final Language<JsTokenId> language; |
| |
| private final boolean singleQuote; |
| |
| /** When != -1, this indicates that we previously adjusted the indentation of the |
| * line to the given offset, and if it turns out that the user changes that token, |
| * we revert to the original indentation |
| */ |
| private int previousAdjustmentOffset = -1; |
| |
| /** |
| * The indentation to revert to when previousAdjustmentOffset is set and the token |
| * changed |
| */ |
| private int previousAdjustmentIndent; |
| |
| public JsTypedTextInterceptor(Language<JsTokenId> language, boolean singleQuote) { |
| this.language = language; |
| this.singleQuote = singleQuote; |
| } |
| |
| private boolean isInsertMatchingEnabled() { |
| EditorOptions options = EditorOptions.get(language.mimeType()); |
| if (options != null) { |
| return options.getMatchBrackets(); |
| } |
| |
| return true; |
| } |
| |
| private boolean isSmartQuotingEnabled() { |
| return OptionsUtils.forLanguage(language).autoCompletionSmartQuotes(); |
| } |
| |
| @Override |
| public void afterInsert(final Context context) throws BadLocationException { |
| final BaseDocument doc = (BaseDocument) context.getDocument(); |
| final AtomicReference<BadLocationException> ex = new AtomicReference<BadLocationException>(); |
| doc.runAtomicAsUser(new Runnable() { |
| |
| @Override |
| public void run() { |
| int dotPos = context.getOffset(); |
| Caret caret = context.getComponent().getCaret(); |
| char ch = context.getText().charAt(0); |
| |
| try { |
| // See if our automatic adjustment of indentation when typing (for example) "end" was |
| // premature - if you were typing a longer word beginning with one of my adjustment |
| // prefixes, such as "endian", then put the indentation back. |
| if (previousAdjustmentOffset != -1) { |
| if (dotPos == previousAdjustmentOffset) { |
| // Revert indentation iff the character at the insert position does |
| // not start a new token (e.g. the previous token that we reindented |
| // was not complete) |
| TokenSequence<? extends JsTokenId> ts = LexUtilities.getTokenSequence( |
| doc, dotPos, language); |
| |
| if (ts != null) { |
| ts.move(dotPos); |
| |
| if (ts.moveNext() && (ts.offset() < dotPos)) { |
| GsfUtilities.setLineIndentation(doc, dotPos, previousAdjustmentIndent); |
| } |
| } |
| } |
| |
| previousAdjustmentOffset = -1; |
| } |
| |
| switch (ch) { |
| case '{': |
| case '(': |
| case '[': |
| if (!isInsertMatchingEnabled()) { |
| break; |
| } |
| case '}': |
| case ')': |
| case ']': |
| Token<? extends JsTokenId> token = LexUtilities.getToken(doc, dotPos, language); |
| if (token == null) { |
| return; |
| } |
| TokenId id = token.id(); |
| |
| if (((id == JsTokenId.IDENTIFIER) && (token.length() == 1)) |
| || (id == JsTokenId.BRACKET_LEFT_BRACKET) || (id == JsTokenId.BRACKET_RIGHT_BRACKET) |
| || (id == JsTokenId.BRACKET_LEFT_CURLY) || (id == JsTokenId.BRACKET_RIGHT_CURLY) |
| || (id == JsTokenId.BRACKET_LEFT_PAREN) || (id == JsTokenId.BRACKET_RIGHT_PAREN)) { |
| if (ch == ']') { |
| skipClosingBracket(doc, caret, ch, JsTokenId.BRACKET_RIGHT_BRACKET); |
| } else if (ch == ')') { |
| skipClosingBracket(doc, caret, ch, JsTokenId.BRACKET_RIGHT_PAREN); |
| } else if (ch == '}') { |
| skipClosingBracket(doc, caret, ch, JsTokenId.BRACKET_RIGHT_CURLY); |
| // the curly is not completed intentionally see #189443 |
| // java and php don't do that as well |
| } else if ((ch == '[') || (ch == '(')) { |
| completeOpeningBracket(doc, dotPos, caret, ch); |
| } |
| } |
| |
| // Reindent blocks (won't do anything if } is not at the beginning of a line |
| if (ch == '}') { |
| reindent(doc, dotPos, JsTokenId.BRACKET_RIGHT_CURLY, caret); |
| } else if (ch == ']') { |
| reindent(doc, dotPos, JsTokenId.BRACKET_RIGHT_BRACKET, caret); |
| } |
| break; |
| default: |
| break; |
| } |
| } catch (BadLocationException blex) { |
| ex.set(blex); |
| } |
| } |
| }); |
| BadLocationException blex = ex.get(); |
| if (blex != null) { |
| throw blex; |
| } |
| } |
| |
| @Override |
| public boolean beforeInsert(Context context) throws BadLocationException { |
| return false; |
| } |
| |
| @Override |
| public void insert(MutableContext context) throws BadLocationException { |
| int caretOffset = context.getOffset(); |
| char ch = context.getText().charAt(0); |
| BaseDocument doc = (BaseDocument) context.getDocument(); |
| String selection = context.getReplacedText(); |
| boolean isTemplate = GsfUtilities.isCodeTemplateEditing(doc); |
| |
| if (selection != null && selection.length() > 0) { |
| if (!isTemplate && (((ch == '"' || ch == '\'' || ch == '`') && isSmartQuotingEnabled()) |
| || ((ch == '(' || ch == '{' || ch == '[')) && isInsertMatchingEnabled())) { |
| // Bracket the selection |
| char firstChar = selection.charAt(0); |
| if (firstChar != ch) { |
| TokenSequence<? extends JsTokenId> ts = LexUtilities.getPositionedSequence( |
| doc, caretOffset, language); |
| if (ts != null |
| && ts.token().id() != JsTokenId.LINE_COMMENT |
| && ts.token().id() != JsTokenId.DOC_COMMENT |
| && ts.token().id() != JsTokenId.BLOCK_COMMENT // not inside comments |
| && ts.token().id() != JsTokenId.STRING) { // not inside strings! |
| int lastChar = selection.charAt(selection.length()-1); |
| // Replace the surround-with chars? |
| if (selection.length() > 1 && |
| ((firstChar == '"' || firstChar == '\'' || firstChar == '`' || firstChar == '(' || |
| firstChar == '{' || firstChar == '[' || firstChar == '/') && |
| lastChar == matching(firstChar))) { |
| String innerText = selection.substring(1, selection.length() - 1); |
| String text = Character.toString(ch) + innerText + Character.toString(matching(ch)); |
| context.setText(text, text.length()); |
| } else { |
| // No, insert around |
| String text = ch + selection + matching(ch); |
| context.setText(text, text.length()); |
| } |
| |
| return; |
| } |
| } |
| } |
| } |
| |
| TokenSequence<? extends JsTokenId> ts = LexUtilities.getTokenSequence( |
| doc, caretOffset, language); |
| |
| if (ts == null) { |
| return; |
| } |
| |
| ts.move(caretOffset); |
| |
| if (!ts.moveNext() && !ts.movePrevious()) { |
| return; |
| } |
| |
| Token<? extends JsTokenId> token = ts.token(); |
| JsTokenId id = token.id(); |
| TokenId[] stringTokens = null; |
| TokenId beginTokenId = null; |
| |
| // "/" is handled AFTER the character has been inserted since we need the lexer's help |
| if (ch == '\"' || (ch == '\'' && singleQuote)) { |
| stringTokens = STRING_TOKENS; |
| beginTokenId = JsTokenId.STRING_BEGIN; |
| } else if (ch == '`' && singleQuote) { |
| stringTokens = TEMPLATE_TOKENS; |
| beginTokenId = JsTokenId.TEMPLATE_BEGIN; |
| } else if (id.isError()) { |
| ts.movePrevious(); |
| |
| TokenId prevId = ts.token().id(); |
| |
| if (isCompletableStringBoundary(ts.token(), singleQuote, false)) { |
| stringTokens = STRING_TOKENS; |
| beginTokenId = prevId; |
| } else if (isCompletableTemplateBoundary(ts.token(), singleQuote, false)) { |
| stringTokens = TEMPLATE_TOKENS; |
| beginTokenId = prevId; |
| } else if (prevId == JsTokenId.REGEXP_BEGIN) { |
| stringTokens = REGEXP_TOKENS; |
| beginTokenId = JsTokenId.REGEXP_BEGIN; |
| } |
| } else if (isCompletableStringBoundary(token, singleQuote, false) && |
| (caretOffset == (ts.offset() + 1))) { |
| if (!Character.isLetter(ch)) { // %q, %x, etc. Only %[], %!!, %<space> etc. is allowed |
| stringTokens = STRING_TOKENS; |
| beginTokenId = id; |
| } |
| } else if (isCompletableTemplateBoundary(token, singleQuote, false) && |
| (caretOffset == (ts.offset() + 1))) { |
| if (!Character.isLetter(ch)) { // %q, %x, etc. Only %[], %!!, %<space> etc. is allowed |
| stringTokens = TEMPLATE_TOKENS; |
| beginTokenId = id; |
| } |
| } else if ((isCompletableStringBoundary(token, singleQuote, false) && (caretOffset == (ts.offset() + 2))) || |
| isCompletableStringBoundary(token, singleQuote, true)) { |
| stringTokens = STRING_TOKENS; |
| beginTokenId = JsTokenId.STRING_BEGIN; |
| } else if ((isCompletableTemplateBoundary(token, singleQuote, false) && (caretOffset == (ts.offset() + 2))) || |
| isCompletableTemplateBoundary(token, singleQuote, true)) { |
| stringTokens = TEMPLATE_TOKENS; |
| beginTokenId = JsTokenId.TEMPLATE_BEGIN; |
| } else if (((id == JsTokenId.REGEXP_BEGIN) && (caretOffset == (ts.offset() + 2))) || |
| (id == JsTokenId.REGEXP_END)) { |
| stringTokens = REGEXP_TOKENS; |
| beginTokenId = JsTokenId.REGEXP_BEGIN; |
| } |
| |
| if (stringTokens != null && isSmartQuotingEnabled()) { |
| completeQuote(context, ch, stringTokens, beginTokenId, isTemplate); |
| } |
| } |
| |
| @Override |
| public void cancelled(Context context) { |
| } |
| |
| private void reindent(BaseDocument doc, int offset, TokenId id, Caret caret) |
| throws BadLocationException { |
| TokenSequence<? extends JsTokenId> ts = LexUtilities.getTokenSequence( |
| doc, offset, language); |
| |
| if (ts != null) { |
| ts.move(offset); |
| |
| if (!ts.moveNext() && !ts.movePrevious()) { |
| return; |
| } |
| |
| Token<? extends JsTokenId> token = ts.token(); |
| |
| if ((token.id() == id)) { |
| final int rowFirstNonWhite = Utilities.getRowFirstNonWhite(doc, offset); |
| // Ensure that this token is at the beginning of the line |
| if (ts.offset() > rowFirstNonWhite) { |
| // if (RubyUtils.isRhtmlDocument(doc)) { |
| // // Allow "<%[whitespace]*" to preceed |
| // String s = doc.getText(rowFirstNonWhite, ts.offset()-rowFirstNonWhite); |
| // if (!s.matches("<%\\s*")) { |
| // return; |
| // } |
| // } else { |
| return; |
| // } |
| } |
| |
| OffsetRange begin = OffsetRange.NONE; |
| |
| if (id == JsTokenId.BRACKET_RIGHT_CURLY) { |
| begin = LexUtilities.findBwd(doc, ts, JsTokenId.BRACKET_LEFT_CURLY, JsTokenId.BRACKET_RIGHT_CURLY); |
| } else if (id == JsTokenId.BRACKET_RIGHT_BRACKET) { |
| begin = LexUtilities.findBwd(doc, ts, JsTokenId.BRACKET_LEFT_BRACKET, JsTokenId.BRACKET_RIGHT_BRACKET); |
| } |
| |
| if (begin != OffsetRange.NONE) { |
| int beginOffset = begin.getStart(); |
| int indent = GsfUtilities.getLineIndent(doc, beginOffset); |
| previousAdjustmentIndent = GsfUtilities.getLineIndent(doc, offset); |
| GsfUtilities.setLineIndentation(doc, offset, indent); |
| previousAdjustmentOffset = caret.getDot(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Check for various conditions and possibly add a pairing bracket |
| * to the already inserted. |
| * @param doc the document |
| * @param dotPos position of the opening bracket (already in the doc) |
| * @param caret caret |
| * @param bracket the bracket that was inserted |
| */ |
| private void completeOpeningBracket(BaseDocument doc, int dotPos, Caret caret, char bracket) |
| throws BadLocationException { |
| if (isCompletablePosition(doc, dotPos + 1)) { |
| String matchingBracket = "" + matching(bracket); |
| doc.insertString(dotPos + 1, matchingBracket, null); |
| caret.setDot(dotPos + 1); |
| } |
| } |
| |
| /** |
| * Check for conditions and possibly complete an already inserted |
| * quote . |
| * @param doc the document |
| * @param dotPos position of the opening bracket (already in the doc) |
| * @param caret caret |
| * @param bracket the character that was inserted |
| */ |
| private void completeQuote(MutableContext context, char bracket, |
| TokenId[] stringTokens, TokenId beginToken, boolean isTemplate) throws BadLocationException { |
| if (isTemplate) { |
| if (bracket == '"' || bracket == '\'' || bracket == '`' || bracket == '(' || bracket == '{' || bracket == '[') { |
| String text = context.getText() + matching(bracket); |
| context.setText(text, text.length() - 1); |
| } |
| return; |
| } |
| int dotPos = context.getOffset(); |
| BaseDocument doc = (BaseDocument) context.getDocument(); |
| if (isEscapeSequence(doc, dotPos)) { // \" or \' typed |
| return; |
| } |
| |
| // Examine token at the caret offset |
| if (doc.getLength() < dotPos) { |
| return; |
| } |
| |
| TokenSequence<? extends JsTokenId> ts = LexUtilities.getTokenSequence( |
| doc, dotPos, language); |
| |
| if (ts == null) { |
| return; |
| } |
| |
| ts.move(dotPos); |
| |
| if (!ts.moveNext() && !ts.movePrevious()) { |
| return; |
| } |
| |
| Token<? extends JsTokenId> token = ts.token(); |
| Token<? extends JsTokenId> previousToken = null; |
| |
| if (ts.movePrevious()) { |
| previousToken = ts.token(); |
| } |
| |
| int lastNonWhite = Utilities.getRowLastNonWhite(doc, dotPos); |
| |
| // eol - true if the caret is at the end of line (ignoring whitespaces) |
| boolean eol = lastNonWhite < dotPos; |
| |
| if ((token.id() == JsTokenId.BLOCK_COMMENT) |
| || (token.id() == JsTokenId.DOC_COMMENT) |
| || (token.id() == JsTokenId.LINE_COMMENT) |
| || (previousToken != null && previousToken.id() == JsTokenId.LINE_COMMENT && token.id() == JsTokenId.EOL)) { |
| return; |
| } else if ((token.id() == JsTokenId.WHITESPACE) && eol && ((dotPos - 1) > 0)) { |
| // check if the caret is at the very end of the line comment |
| token = LexUtilities.getToken(doc, dotPos - 1, language); |
| |
| if (token.id() == JsTokenId.LINE_COMMENT) { |
| return; |
| } |
| } |
| |
| boolean completablePosition = isQuoteCompletablePosition(doc, dotPos); |
| |
| boolean insideString = false; |
| JsTokenId id = token.id(); |
| |
| for (TokenId currId : stringTokens) { |
| if (id == currId) { |
| insideString = true; |
| break; |
| } |
| } |
| |
| if (id.isError() && (previousToken != null) |
| && (previousToken.id() == beginToken)) { |
| insideString = true; |
| } |
| |
| if (id == JsTokenId.EOL && previousToken != null) { |
| if (previousToken.id() == beginToken) { |
| insideString = true; |
| } else if (previousToken.id().isError()) { |
| if (ts.movePrevious()) { |
| if (ts.token().id() == beginToken) { |
| insideString = true; |
| } |
| } |
| } |
| } |
| |
| if (!insideString) { |
| // check if the caret is at the very end of the line and there |
| // is an unterminated string literal |
| if ((token.id() == JsTokenId.WHITESPACE) && eol) { |
| if ((dotPos - 1) > 0) { |
| token = LexUtilities.getToken(doc, dotPos - 1, language); |
| // XXX TODO use language embedding to handle this |
| insideString = (token.id() == JsTokenId.STRING || token.id() == JsTokenId.TEMPLATE); |
| } |
| } |
| } |
| |
| if (insideString) { |
| if (eol) { |
| return; // do not complete |
| } else { |
| //#69524 |
| char chr = doc.getChars(dotPos, 1)[0]; |
| |
| if (chr == bracket) { |
| context.setText(Character.toString(bracket), 1); |
| doc.remove(dotPos, 1); |
| |
| return; |
| } |
| } |
| } |
| |
| if ((completablePosition && !insideString) || eol) { |
| String text = Character.toString(bracket) + matching(bracket); |
| context.setText(text, text.length() - 1); |
| } |
| } |
| |
| /** |
| * Checks whether dotPos is a position at which bracket and quote |
| * completion is performed. Brackets and quotes are not completed |
| * everywhere but just at suitable places . |
| * @param doc the document |
| * @param dotPos position to be tested |
| */ |
| private boolean isCompletablePosition(BaseDocument doc, int dotPos) |
| throws BadLocationException { |
| if (dotPos == doc.getLength()) { // there's no other character to test |
| |
| return true; |
| } else { |
| // test that we are in front of ) , " or ' |
| char chr = doc.getChars(dotPos, 1)[0]; |
| |
| return ((chr == ')') || (chr == ',') || (chr == '\"') || (chr == '\'') || (chr == '`') || (chr == ' ') || |
| (chr == ']') || (chr == '}') || (chr == '\n') || (chr == '\t') || (chr == ';')); |
| } |
| } |
| |
| private boolean isQuoteCompletablePosition(BaseDocument doc, int dotPos) |
| throws BadLocationException { |
| if (dotPos == doc.getLength()) { // there's no other character to test |
| |
| return true; |
| } else { |
| // test that we are in front of ) , " or ' ... etc. |
| int eol = Utilities.getRowEnd(doc, dotPos); |
| |
| if ((dotPos == eol) || (eol == -1)) { |
| return false; |
| } |
| |
| int firstNonWhiteFwd = Utilities.getFirstNonWhiteFwd(doc, dotPos, eol); |
| |
| if (firstNonWhiteFwd == -1) { |
| return false; |
| } |
| |
| char chr = doc.getChars(firstNonWhiteFwd, 1)[0]; |
| |
| return ((chr == ')') || (chr == ',') || (chr == '+') || (chr == '}') || (chr == ';') || |
| (chr == ']')); |
| } |
| } |
| |
| /** |
| * A hook to be called after closing bracket ) or ] was inserted into |
| * the document. The method checks if the bracket should stay there |
| * or be removed and some exisitng bracket just skipped. |
| * |
| * @param doc the document |
| * @param dotPos position of the inserted bracket |
| * @param caret caret |
| * @param bracket the bracket character ']' or ')' |
| */ |
| private void skipClosingBracket(BaseDocument doc, Caret caret, char bracket, TokenId bracketId) |
| throws BadLocationException { |
| int caretOffset = caret.getDot(); |
| |
| if (isSkipClosingBracket(doc, caretOffset, bracketId)) { |
| doc.remove(caretOffset - 1, 1); |
| caret.setDot(caretOffset); // skip closing bracket |
| } |
| } |
| |
| /** |
| * Check whether the typed bracket should stay in the document |
| * or be removed. |
| * <br> |
| * This method is called by <code>skipClosingBracket()</code>. |
| * |
| * @param doc document into which typing was done. |
| * @param caretOffset |
| */ |
| private boolean isSkipClosingBracket(BaseDocument doc, int caretOffset, TokenId bracketId) |
| throws BadLocationException { |
| // First check whether the caret is not after the last char in the document |
| // because no bracket would follow then so it could not be skipped. |
| if (caretOffset == doc.getLength()) { |
| return false; // no skip in this case |
| } |
| |
| boolean skipClosingBracket = false; // by default do not remove |
| |
| TokenSequence<? extends JsTokenId> ts = LexUtilities.getTokenSequence( |
| doc, caretOffset, language); |
| |
| if (ts == null) { |
| return false; |
| } |
| |
| // XXX BEGIN TOR MODIFICATIONS |
| //ts.move(caretOffset+1); |
| ts.move(caretOffset); |
| |
| if (!ts.moveNext()) { |
| return false; |
| } |
| |
| Token<? extends JsTokenId> token = ts.token(); |
| |
| // Check whether character follows the bracket is the same bracket |
| if ((token != null) && (token.id() == bracketId)) { |
| int bracketIntId = bracketId.ordinal(); |
| int leftBracketIntId = |
| (bracketIntId == JsTokenId.BRACKET_RIGHT_PAREN.ordinal()) ? JsTokenId.BRACKET_LEFT_PAREN.ordinal() |
| : JsTokenId.BRACKET_LEFT_BRACKET.ordinal(); |
| |
| // Skip all the brackets of the same type that follow the last one |
| ts.moveNext(); |
| |
| Token<? extends JsTokenId> nextToken = ts.token(); |
| boolean endOfJs = false; |
| while ((nextToken != null) && (nextToken.id() == bracketId)) { |
| token = nextToken; |
| |
| if (!ts.moveNext()) { |
| endOfJs = true; |
| break; |
| } |
| |
| nextToken = ts.token(); |
| } |
| |
| // token var points to the last bracket in a group of two or more right brackets |
| // Attempt to find the left matching bracket for it |
| // Search would stop on an extra opening left brace if found |
| int braceBalance = 0; // balance of '{' and '}' |
| int bracketBalance = 0; // balance of the brackets or parenthesis |
| Token<? extends JsTokenId> lastRBracket = token; |
| if (!endOfJs) { |
| // move on the las bracket || parent |
| ts.movePrevious(); |
| } |
| token = ts.token(); |
| |
| boolean finished = false; |
| |
| while (!finished && (token != null)) { |
| int tokenIntId = token.id().ordinal(); |
| |
| if ((token.id() == JsTokenId.BRACKET_LEFT_PAREN) || (token.id() == JsTokenId.BRACKET_LEFT_BRACKET)) { |
| if (tokenIntId == leftBracketIntId) { |
| bracketBalance++; |
| |
| if (bracketBalance == 0) { |
| if (braceBalance != 0) { |
| // Here the bracket is matched but it is located |
| // inside an unclosed brace block |
| // e.g. ... ->( } a()|) |
| // which is in fact illegal but it's a question |
| // of what's best to do in this case. |
| // We chose to leave the typed bracket |
| // by setting bracketBalance to 1. |
| // It can be revised in the future. |
| bracketBalance = 1; |
| } |
| |
| finished = true; |
| } |
| } |
| } else if ((token.id() == JsTokenId.BRACKET_RIGHT_PAREN) || |
| (token.id() == JsTokenId.BRACKET_RIGHT_BRACKET)) { |
| if (tokenIntId == bracketIntId) { |
| bracketBalance--; |
| } |
| } else if (token.id() == JsTokenId.BRACKET_LEFT_CURLY) { |
| braceBalance++; |
| |
| if (braceBalance > 0) { // stop on extra left brace |
| finished = true; |
| } |
| } else if (token.id() == JsTokenId.BRACKET_RIGHT_CURLY) { |
| braceBalance--; |
| } |
| |
| if (!ts.movePrevious()) { |
| break; |
| } |
| |
| token = ts.token(); |
| } |
| |
| if (bracketBalance != 0 |
| || (bracketId == JsTokenId.BRACKET_RIGHT_CURLY && braceBalance < 0)) { // not found matching bracket |
| // Remove the typed bracket as it's unmatched |
| skipClosingBracket = true; |
| } else { // the bracket is matched |
| // Now check whether the bracket would be matched |
| // when the closing bracket would be removed |
| // i.e. starting from the original lastRBracket token |
| // and search for the same bracket to the right in the text |
| // The search would stop on an extra right brace if found |
| braceBalance = 0; |
| bracketBalance = 0; |
| |
| //token = lastRBracket.getNext(); |
| TokenHierarchy<BaseDocument> th = TokenHierarchy.get(doc); |
| |
| int ofs = lastRBracket.offset(th); |
| |
| ts.move(ofs); |
| ts.moveNext(); |
| token = ts.token(); |
| finished = false; |
| |
| while (!finished && (token != null)) { |
| if ((token.id() == JsTokenId.BRACKET_LEFT_PAREN) || (token.id() == JsTokenId.BRACKET_LEFT_BRACKET)) { |
| if (token.id().ordinal() == leftBracketIntId) { |
| bracketBalance++; |
| } |
| } else if ((token.id() == JsTokenId.BRACKET_RIGHT_PAREN) || |
| (token.id() == JsTokenId.BRACKET_RIGHT_BRACKET)) { |
| if (token.id().ordinal() == bracketIntId) { |
| bracketBalance--; |
| |
| if (bracketBalance == 0) { |
| if (braceBalance != 0) { |
| // Here the bracket is matched but it is located |
| // inside an unclosed brace block |
| // which is in fact illegal but it's a question |
| // of what's best to do in this case. |
| // We chose to leave the typed bracket |
| // by setting bracketBalance to -1. |
| // It can be revised in the future. |
| bracketBalance = -1; |
| } |
| |
| finished = true; |
| } |
| } |
| } else if (token.id() == JsTokenId.BRACKET_LEFT_CURLY) { |
| braceBalance++; |
| } else if (token.id() == JsTokenId.BRACKET_RIGHT_CURLY) { |
| braceBalance--; |
| } |
| |
| if (!ts.movePrevious()) { |
| break; |
| } |
| |
| token = ts.token(); |
| } |
| |
| skipClosingBracket = ((braceBalance == 0) && (bracketId == JsTokenId.BRACKET_RIGHT_CURLY)) |
| || ((bracketBalance > 0) && (bracketId == JsTokenId.BRACKET_RIGHT_BRACKET || bracketId == JsTokenId.BRACKET_RIGHT_PAREN)); |
| } |
| } |
| |
| return skipClosingBracket; |
| } |
| |
| // XXX TODO Use embedded string sequence here and see if it |
| // really is escaped. I know where those are! |
| // TODO Adjust for JavaScript |
| private static boolean isEscapeSequence(BaseDocument doc, int dotPos) |
| throws BadLocationException { |
| if (dotPos <= 0) { |
| return false; |
| } |
| |
| char previousChar = doc.getChars(dotPos - 1, 1)[0]; |
| |
| return previousChar == '\\'; |
| } |
| |
| /** |
| * Returns for an opening bracket or quote the appropriate closing |
| * character. |
| */ |
| private static char matching(char bracket) { |
| switch (bracket) { |
| case '(': |
| return ')'; |
| |
| case '[': |
| return ']'; |
| |
| case '\"': |
| return '\"'; // NOI18N |
| |
| case '\'': |
| return '\''; |
| |
| case '`': |
| return '`'; |
| |
| case '{': |
| return '}'; |
| |
| case '}': |
| return '{'; |
| |
| default: |
| return bracket; |
| } |
| } |
| |
| private static boolean isCompletableStringBoundary(Token<? extends JsTokenId> token, |
| boolean singleQuote, boolean end) { |
| if ((!end && token.id() == JsTokenId.STRING_BEGIN) |
| || (end && token.id() == JsTokenId.STRING_END)) { |
| if (singleQuote || "\"".equals(token.text().toString())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static boolean isCompletableTemplateBoundary(Token<? extends JsTokenId> token, |
| boolean singleQuote, boolean end) { |
| if ((!end && token.id() == JsTokenId.TEMPLATE_BEGIN) |
| || (end && token.id() == JsTokenId.TEMPLATE_END)) { |
| return singleQuote; |
| } |
| return false; |
| } |
| |
| @MimeRegistration(mimeType = JsTokenId.JAVASCRIPT_MIME_TYPE, service = TypedTextInterceptor.Factory.class) |
| public static class JsFactory implements TypedTextInterceptor.Factory { |
| |
| @Override |
| public TypedTextInterceptor createTypedTextInterceptor(org.netbeans.api.editor.mimelookup.MimePath mimePath) { |
| return new JsTypedTextInterceptor(JsTokenId.javascriptLanguage(), true); |
| } |
| |
| } |
| |
| @MimeRegistration(mimeType = JsTokenId.JSON_MIME_TYPE, service = TypedTextInterceptor.Factory.class) |
| public static class JsonFactory implements TypedTextInterceptor.Factory { |
| |
| @Override |
| public TypedTextInterceptor createTypedTextInterceptor(org.netbeans.api.editor.mimelookup.MimePath mimePath) { |
| return new JsTypedTextInterceptor(JsTokenId.jsonLanguage(), false); |
| } |
| |
| } |
| } |