| /* |
| * 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.Optional; |
| import java.util.function.Predicate; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import javax.swing.text.BadLocationException; |
| import javax.swing.text.Document; |
| import org.netbeans.api.editor.document.EditorDocumentUtils; |
| import org.netbeans.api.editor.mimelookup.MimePath; |
| import org.netbeans.api.editor.mimelookup.MimeRegistration; |
| import org.netbeans.api.editor.mimelookup.MimeRegistrations; |
| 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.lib.editor.util.CharSequenceUtilities; |
| import org.netbeans.modules.csl.api.EditorOptions; |
| import org.netbeans.modules.csl.spi.GsfUtilities; |
| import org.netbeans.modules.editor.indent.api.IndentUtils; |
| import org.netbeans.modules.javascript2.editor.doc.JsDocumentationCompleter; |
| import org.netbeans.modules.javascript2.json.api.JsonOptionsQuery; |
| import org.netbeans.modules.javascript2.lexer.api.JsDocumentationTokenId; |
| import org.netbeans.modules.javascript2.lexer.api.JsTokenId; |
| import org.netbeans.modules.javascript2.lexer.api.LexUtilities; |
| import org.netbeans.spi.editor.typinghooks.TypedBreakInterceptor; |
| import org.openide.util.Exceptions; |
| |
| /** |
| * |
| * @author Petr Hejl |
| */ |
| public class JsTypedBreakInterceptor implements TypedBreakInterceptor { |
| |
| /** |
| * When true, continue comments if you press return in a line comment |
| * (that does not also have code on the same line). |
| */ |
| static final boolean CONTINUE_COMMENTS = Boolean.getBoolean("js.cont.comment"); // NOI18N |
| |
| // unit testing |
| static boolean completeDocumentation = true; |
| |
| private static final Logger LOGGER = Logger.getLogger(JsTypedBreakInterceptor.class.getName()); |
| |
| private final Language<JsTokenId> language; |
| |
| private final Predicate<Document> comments; |
| |
| private final boolean multiLineLiterals; |
| |
| private CommentGenerator commentGenerator = null; |
| |
| public JsTypedBreakInterceptor(Language<JsTokenId> language, boolean comments, boolean multiLineLiterals) { |
| this(language, (doc) -> comments, multiLineLiterals); |
| } |
| |
| public JsTypedBreakInterceptor(Language<JsTokenId> language, Predicate<Document> comments, boolean multiLineLiterals) { |
| this.language = language; |
| this.comments = comments; |
| this.multiLineLiterals = multiLineLiterals; |
| } |
| |
| private boolean isInsertMatchingEnabled() { |
| EditorOptions options = EditorOptions.get(language.mimeType()); |
| if (options != null) { |
| return options.getMatchBrackets(); |
| } |
| |
| return true; |
| } |
| |
| @Override |
| public void insert(MutableContext context) throws BadLocationException { |
| BaseDocument doc = (BaseDocument) context.getDocument(); |
| TokenHierarchy<BaseDocument> tokenHierarchy = TokenHierarchy.get(doc); |
| int offset = context.getCaretOffset(); |
| |
| int lineBegin = Utilities.getRowStart(doc, offset); |
| int lineEnd = Utilities.getRowEnd(doc, offset); |
| |
| if (lineBegin == offset && lineEnd == offset) { |
| // Pressed return on a blank newline - do nothing |
| return; |
| } |
| |
| TokenSequence<? extends JsTokenId> ts = LexUtilities.getTokenSequence( |
| tokenHierarchy, offset, language); |
| |
| if (ts == null) { |
| return; |
| } |
| |
| ts.move(offset); |
| |
| if (!ts.moveNext() && !ts.movePrevious()) { |
| return; |
| } |
| |
| Token<? extends JsTokenId> token = ts.token(); |
| JsTokenId id = token.id(); |
| |
| // Insert a missing } |
| if (!id.isError() && isInsertMatchingEnabled() && !isDocToken(id) && isAddRightBrace(doc, offset)) { |
| int indent = GsfUtilities.getLineIndent(doc, offset); |
| |
| int afterLastNonWhite = Utilities.getRowLastNonWhite(doc, offset); |
| |
| // We've either encountered a further indented line, or a line that doesn't |
| // look like the end we're after, so insert a matching end. |
| StringBuilder sb = new StringBuilder(); |
| int carretOffset = 0; |
| int curlyOffset = getUnbalancedCurlyOffset(doc, offset); |
| if (offset > afterLastNonWhite) { |
| |
| sb.append("\n"); // XXX On Windows, do \r\n? |
| sb.append(IndentUtils.createIndentString(doc, indent + IndentUtils.indentLevelSize(doc))); |
| carretOffset = sb.length(); |
| sb.append("\n"); // NOI18N |
| if (curlyOffset >= 0) { |
| sb.append(IndentUtils.createIndentString(doc, getCurlyIndent(doc, curlyOffset))); |
| } else { |
| sb.append(IndentUtils.createIndentString(doc, indent)); |
| } |
| sb.append("}"); // NOI18N |
| } else { |
| boolean insert[] = {true}; |
| int end = getRowOrBlockEnd(doc, offset, insert); |
| if (insert[0]) { |
| // I'm inserting a newline in the middle of a sentence, such as the scenario in #118656 |
| // I should insert the end AFTER the text on the line |
| String restOfLine = doc.getText(offset, |
| Math.min(end, Utilities.getRowEnd(doc, afterLastNonWhite)) - offset); |
| sb.append("\n"); // XXX On Windows, do \r\n? |
| sb.append(IndentUtils.createIndentString(doc, indent + IndentUtils.indentLevelSize(doc))); |
| // right brace must be included into the correct context - issue #219683 |
| carretOffset = sb.length(); |
| |
| sb.append(restOfLine); // NOI18N |
| sb.append("\n"); // NOI18N |
| if (curlyOffset >= 0) { |
| sb.append(IndentUtils.createIndentString(doc, getCurlyIndent(doc, curlyOffset))); |
| } else { |
| sb.append(IndentUtils.createIndentString(doc, indent)); |
| } |
| sb.append("}"); // NOI18N |
| doc.remove(offset, restOfLine.length()); |
| } |
| |
| } |
| |
| if (sb.length() > 0) { |
| context.setText(sb.toString(), 0, carretOffset); |
| } |
| return; |
| } |
| |
| if (id.isError()) { |
| // See if it's a block comment opener |
| String text = token.text().toString(); |
| if (comments.test(doc) && text.startsWith("/*") ) { |
| int indent = GsfUtilities.getLineIndent(doc, ts.offset()); |
| StringBuilder sb = new StringBuilder(); |
| sb.append("\n"); // NOI18N |
| sb.append(IndentUtils.createIndentString(doc, indent)); |
| sb.append(" * "); // NOI18N |
| int carretOffset = sb.length(); |
| sb.append("\n"); // NOI18N |
| sb.append(IndentUtils.createIndentString(doc, indent)); |
| sb.append(" */"); // NOI18N |
| |
| if (text.startsWith("/**")) { |
| // setup comment generator |
| commentGenerator = new CommentGenerator(offset + carretOffset, indent + 1); |
| } |
| context.setText(sb.toString(), 0, carretOffset); |
| return; |
| } |
| } |
| |
| if (multiLineLiterals) { |
| if (id == JsTokenId.STRING || |
| (id == JsTokenId.STRING_END) && offset < ts.offset()+ts.token().length()) { |
| // Instead of splitting a string "foobar" into "foo"+"bar", just insert a \ instead! |
| //int indent = GsfUtilities.getLineIndent(doc, offset); |
| //int delimiterOffset = id == JsTokenId.STRING_END ? ts.offset() : ts.offset()-1; |
| //char delimiter = doc.getText(delimiterOffset,1).charAt(0); |
| //doc.insertString(offset, delimiter + " + " + delimiter, null); |
| //caret.setDot(offset+3); |
| //return offset + 5 + indent; |
| String str = "\\\n"; //NOI18N |
| if (id != JsTokenId.STRING || offset > ts.offset()) { |
| str = "\\n\\\n"; //NOI18N |
| if (offset - ts.offset() < ts.token().length()) { |
| String text = ts.token().text().toString(); |
| text = text.substring(0, offset - ts.offset()); |
| if(text.endsWith("\\n\\")) { //NOI18N |
| str = "\n\\n\\"; //NOI18N |
| } |
| } |
| } |
| context.setText(str, -1, str.length()); |
| return; |
| } |
| if (id == JsTokenId.TEMPLATE || |
| (id == JsTokenId.TEMPLATE_END) && offset < ts.offset()+ts.token().length()) { |
| // Instead of indenting it to the previous line as below just insert a newline and finish! |
| String str = "\n"; //NOI18N |
| context.setText(str, -1, str.length()); |
| return; |
| } |
| |
| |
| |
| if (id == JsTokenId.REGEXP || |
| (id == JsTokenId.REGEXP_END) && offset < ts.offset()+ts.token().length()) { |
| // Instead of splitting a string "foobar" into "foo"+"bar", just insert a \ instead! |
| //int indent = GsfUtilities.getLineIndent(doc, offset); |
| //doc.insertString(offset, "/ + /", null); |
| //caret.setDot(offset+3); |
| //return offset + 5 + indent; |
| String str = (id != JsTokenId.REGEXP || offset > ts.offset()) ? "\\n\\\n" : "\\\n"; |
| context.setText(str, -1, str.length()); |
| return; |
| } |
| } else { |
| final int indent = GsfUtilities.getLineIndent(doc, offset); |
| final StringBuilder sb = new StringBuilder(); |
| sb.append("\n"); // NOI18N |
| sb.append(IndentUtils.createIndentString(doc, indent + IndentUtils.indentLevelSize(doc))); |
| final int carretOffset = sb.length(); |
| context.setText(sb.toString(), 0, carretOffset); |
| return; |
| } |
| |
| // Special case: since I do hash completion, if you try to type |
| // y = Thread.start { |
| // code here |
| // } |
| // you end up with |
| // y = Thread.start {|} |
| // If you hit newline at this point, you end up with |
| // y = Thread.start { |
| // |} |
| // which is not as helpful as it would be if we were not doing hash-matching |
| // (in that case we'd notice the brace imbalance, and insert the closing |
| // brace on the line below the insert position, and indent properly. |
| // Catch this scenario and handle it properly. |
| if ((id == JsTokenId.BRACKET_RIGHT_CURLY || id == JsTokenId.BRACKET_RIGHT_BRACKET) && offset > 0) { |
| Token<? extends JsTokenId> prevToken = LexUtilities.getToken(doc, offset - 1, language); |
| if (prevToken != null) { |
| JsTokenId prevTokenId = prevToken.id(); |
| if (id == JsTokenId.BRACKET_RIGHT_CURLY && prevTokenId == JsTokenId.BRACKET_LEFT_CURLY || |
| id == JsTokenId.BRACKET_RIGHT_BRACKET && prevTokenId == JsTokenId.BRACKET_LEFT_BRACKET) { |
| int indent = GsfUtilities.getLineIndent(doc, offset); |
| StringBuilder sb = new StringBuilder(); |
| // XXX On Windows, do \r\n? |
| sb.append("\n"); // NOI18N |
| sb.append(IndentUtils.createIndentString(doc, indent + IndentUtils.indentLevelSize(doc))); |
| int carretOffset = sb.length(); |
| sb.append("\n"); // NOI18N |
| sb.append(IndentUtils.createIndentString(doc, indent)); |
| |
| // should we reindent it automatically ? |
| context.setText(sb.toString(), 0, carretOffset); |
| return; |
| } |
| } |
| } |
| |
| if (!comments.test(doc)) { |
| return; |
| } |
| if (id == JsTokenId.WHITESPACE) { |
| // Pressing newline in the whitespace before a comment |
| // should be identical to pressing newline with the caret |
| // at the beginning of the comment |
| int begin = Utilities.getRowFirstNonWhite(doc, offset); |
| if (begin != -1 && offset < begin) { |
| ts.move(begin); |
| if (ts.moveNext()) { |
| id = ts.token().id(); |
| if (id == JsTokenId.LINE_COMMENT) { |
| offset = begin; |
| } |
| } |
| } |
| } |
| |
| if ((id == JsTokenId.BLOCK_COMMENT || id == JsTokenId.DOC_COMMENT) |
| && offset > ts.offset() && offset < ts.offset()+ts.token().length()) { |
| // Continue *'s |
| int begin = Utilities.getRowFirstNonWhite(doc, offset); |
| int end = Utilities.getRowEnd(doc, offset)+1; |
| if (begin == -1) { |
| begin = end; |
| } |
| String line = doc.getText(begin, end-begin); |
| boolean isBlockStart = line.startsWith("/*") || (begin != -1 && begin < ts.offset()); |
| if (isBlockStart || line.startsWith("*")) { |
| int indent = GsfUtilities.getLineIndent(doc, offset); |
| StringBuilder sb = new StringBuilder("\n"); |
| if (isBlockStart) { |
| indent++; |
| } |
| int carretPosition; |
| sb.append(IndentUtils.createIndentString(doc, indent)); |
| if (isBlockStart) { |
| // First comment should be propertly indented |
| sb.append("* "); //NOI18N |
| carretPosition = sb.length(); |
| |
| TokenSequence<? extends JsDocumentationTokenId> jsDocTS = |
| LexUtilities.getJsDocumentationTokenSequence(tokenHierarchy, offset); |
| if (jsDocTS != null) { |
| if (!hasCommentEnd(jsDocTS)) { |
| // setup comment generator |
| commentGenerator = new CommentGenerator(offset + carretPosition, indent); |
| // append end of the comment |
| sb.append("\n").append(IndentUtils.createIndentString(doc, indent)).append("*/"); //NOI18N |
| } |
| } |
| } else { |
| // Copy existing indentation inside the block |
| sb.append("*"); //NOI18N |
| int afterStar = isBlockStart ? begin+2 : begin+1; |
| line = doc.getText(afterStar, Utilities.getRowEnd(doc, afterStar)-afterStar); |
| for (int i = 0; i < line.length(); i++) { |
| char c = line.charAt(i); |
| if (c == ' ' || c == '\t') { //NOI18N |
| sb.append(c); |
| } else { |
| break; |
| } |
| } |
| carretPosition = sb.length(); |
| } |
| |
| if (offset == begin && offset > 0) { |
| context.setText(sb.toString(), -1, sb.length()); |
| return; |
| } |
| context.setText(sb.toString(), -1, carretPosition); |
| return; |
| } |
| } |
| |
| boolean isComment = id == JsTokenId.LINE_COMMENT; |
| if (id == JsTokenId.EOL) { |
| if (ts.movePrevious() && ts.token().id() == JsTokenId.LINE_COMMENT) { |
| //ts.moveNext(); |
| isComment = true; |
| } |
| } |
| |
| if (isComment) { |
| // Only do this if the line only contains comments OR if there is content to the right on this line, |
| // or if the next line is a comment! |
| |
| boolean continueComment = false; |
| int begin = Utilities.getRowFirstNonWhite(doc, offset); |
| |
| // We should only continue comments if the previous line had a comment |
| // (and a comment from the beginning, not a trailing comment) |
| boolean previousLineWasComment = false; |
| boolean nextLineIsComment = false; |
| int rowStart = Utilities.getRowStart(doc, offset); |
| if (rowStart > 0) { |
| int prevBegin = Utilities.getRowFirstNonWhite(doc, rowStart - 1); |
| if (prevBegin != -1) { |
| Token<? extends JsTokenId> firstToken = LexUtilities.getToken( |
| doc, prevBegin, language); |
| if (firstToken != null && firstToken.id() == JsTokenId.LINE_COMMENT) { |
| previousLineWasComment = true; |
| } |
| } |
| } |
| int rowEnd = Utilities.getRowEnd(doc, offset); |
| if (rowEnd < doc.getLength()) { |
| int nextBegin = Utilities.getRowFirstNonWhite(doc, rowEnd + 1); |
| if (nextBegin != -1) { |
| Token<? extends JsTokenId> firstToken = LexUtilities.getToken( |
| doc, nextBegin, language); |
| if (firstToken != null && firstToken.id() == JsTokenId.LINE_COMMENT) { |
| nextLineIsComment = true; |
| } |
| } |
| } |
| |
| // See if we have more input on this comment line (to the right |
| // of the inserted newline); if so it's a "split" operation on |
| // the comment |
| if (previousLineWasComment || nextLineIsComment |
| || (offset > ts.offset() && offset < ts.offset() + ts.token().length())) { |
| if (ts.offset() + token.length() > offset + 1) { |
| // See if the remaining text is just whitespace |
| String trailing = doc.getText(offset, Utilities.getRowEnd(doc, offset) - offset); |
| if (trailing.trim().length() != 0) { |
| continueComment = true; |
| } |
| } else if (CONTINUE_COMMENTS) { |
| // See if the "continue comments" options is turned on, and this is a line that |
| // contains only a comment (after leading whitespace) |
| Token<? extends JsTokenId> firstToken = LexUtilities.getToken( |
| doc, begin, language); |
| if (firstToken.id() == JsTokenId.LINE_COMMENT) { |
| continueComment = true; |
| } |
| } |
| if (!continueComment) { |
| // See if the next line is a comment; if so we want to continue |
| // comments editing the middle of the comment |
| int nextLine = Utilities.getRowEnd(doc, offset) + 1; |
| if (nextLine < doc.getLength()) { |
| int nextLineFirst = Utilities.getRowFirstNonWhite(doc, nextLine); |
| if (nextLineFirst != -1) { |
| Token<? extends JsTokenId> firstToken = LexUtilities.getToken( |
| doc, nextLineFirst, language); |
| if (firstToken != null && firstToken.id() == JsTokenId.LINE_COMMENT) { |
| continueComment = true; |
| } |
| } |
| } |
| } |
| } |
| |
| if (continueComment) { |
| // Line comments should continue |
| int indent = GsfUtilities.getLineIndent(doc, offset); |
| StringBuilder sb = new StringBuilder(); |
| if (offset != begin || offset <= 0) { |
| sb.append("\n"); |
| } |
| sb.append(IndentUtils.createIndentString(doc, indent)); |
| sb.append("//"); // NOI18N |
| // Copy existing indentation |
| int afterSlash = begin + 2; |
| String line = doc.getText(afterSlash, Utilities.getRowEnd(doc, afterSlash) - afterSlash); |
| for (int i = 0; i < line.length(); i++) { |
| char c = line.charAt(i); |
| if (c == ' ' || c == '\t') { |
| sb.append(c); |
| } else { |
| break; |
| } |
| } |
| |
| if (offset == begin && offset > 0) { |
| int caretPosition = sb.length(); |
| sb.append("\n"); |
| context.setText(sb.toString(), -1, caretPosition); |
| return; |
| } |
| context.setText(sb.toString(), -1, sb.length()); |
| return; |
| } |
| } |
| |
| // Just indent the line properly |
| int indentSize = getNextLineIndentation(doc, offset); |
| if (indentSize > 0) { |
| StringBuilder sb = new StringBuilder("\n"); // NOI18N |
| sb.append(IndentUtils.createIndentString(doc, indentSize)); |
| context.setText(sb.toString(), -1, sb.length()); |
| } |
| } |
| |
| @Override |
| public void afterInsert(Context context) throws BadLocationException { |
| if (completeDocumentation && commentGenerator != null) { |
| JsDocumentationCompleter.generateCompleteComment( |
| (BaseDocument) context.getDocument(), |
| commentGenerator.getOffset(), |
| commentGenerator.getIndent()); |
| commentGenerator = null; |
| } |
| } |
| |
| @Override |
| public boolean beforeInsert(Context context) throws BadLocationException { |
| return false; |
| } |
| |
| @Override |
| public void cancelled(Context context) { |
| } |
| |
| /** |
| * Computes the indentation of the next line (after the line break). |
| * |
| * @param doc document |
| * @param offset current offset |
| * @return indentation size |
| * @throws BadLocationException |
| */ |
| private int getNextLineIndentation(BaseDocument doc, int offset) throws BadLocationException { |
| int indent = GsfUtilities.getLineIndent(doc, offset); |
| int currentOffset = offset; |
| while (currentOffset > 0) { |
| if (!Utilities.isRowEmpty(doc, currentOffset) && !Utilities.isRowWhite(doc, currentOffset) |
| && !isCommentOnlyLine(doc, currentOffset, language)) { |
| indent = GsfUtilities.getLineIndent(doc, currentOffset); |
| int parenBalance = getLineBalance(doc, currentOffset, |
| JsTokenId.BRACKET_LEFT_PAREN, JsTokenId.BRACKET_RIGHT_PAREN); |
| if (parenBalance < 0) { |
| break; |
| } |
| int curlyBalance = getLineBalance(doc, currentOffset, |
| JsTokenId.BRACKET_LEFT_CURLY, JsTokenId.BRACKET_RIGHT_CURLY); |
| if (curlyBalance > 0) { |
| indent += IndentUtils.indentLevelSize(doc); |
| } |
| return indent; |
| } |
| currentOffset = Utilities.getRowStart(doc, currentOffset) - 1; |
| } |
| |
| return indent; |
| } |
| |
| /** |
| * From Java. |
| * |
| * Resolve whether pairing right curly should be added automatically |
| * at the caret position or not. |
| * <br> |
| * There must be only whitespace or line comment or block comment |
| * between the caret position |
| * and the left brace and the left brace must be on the same line |
| * where the caret is located. |
| * <br> |
| * The caret must not be "contained" in the opened block comment token. |
| * |
| * @param doc document in which to operate. |
| * @param caretOffset offset of the caret. |
| * @return true if a right brace '}' should be added |
| * or false if not. |
| */ |
| private boolean isAddRightBrace(BaseDocument doc, int caretOffset) throws BadLocationException { |
| TokenSequence<? extends JsTokenId> ts = LexUtilities.getTokenSequence(doc, caretOffset, language); |
| if (ts == null) { |
| return false; |
| } |
| |
| // get balance from start index of the tokenSequence |
| ts.moveIndex(0); |
| if (!ts.moveNext()) { |
| return false; |
| } |
| |
| int balance = 0; |
| boolean balancedAfter = false; |
| |
| do { |
| Token t = ts.token(); |
| |
| if (t.id() == JsTokenId.BRACKET_LEFT_CURLY) { |
| balance++; |
| } else if (t.id() == JsTokenId.BRACKET_RIGHT_CURLY) { |
| balance--; |
| } |
| } while (ts.offset() < caretOffset && ts.moveNext()); |
| |
| for (TokenSequenceIterator tsi = new TokenSequenceIterator(TokenHierarchy.get(doc).tokenSequenceList(ts.languagePath(), caretOffset, doc.getLength()), false); tsi.hasMore();) { |
| TokenSequence<?> sq = tsi.getSequence(); |
| Token t = sq.token(); |
| |
| if (t.id() == JsTokenId.BRACKET_LEFT_CURLY) { |
| balance++; |
| } else if (t.id() == JsTokenId.BRACKET_RIGHT_CURLY) { |
| balance--; |
| } |
| if (balance == 0 |
| && (t.id() == JsTokenId.BRACKET_LEFT_CURLY || t.id() == JsTokenId.BRACKET_RIGHT_CURLY)) { |
| balancedAfter = true; |
| break; |
| } |
| } |
| |
| if (balance < 0) { |
| return false; |
| } |
| |
| int caretRowStartOffset = org.netbeans.editor.Utilities.getRowStart(doc, caretOffset); |
| ts = LexUtilities.getPositionedSequence(doc, caretOffset, language); |
| if (ts == null) { |
| return false; |
| } |
| if (ts.offset() == caretOffset && !ts.movePrevious()) { |
| return false; |
| } |
| boolean first = true; |
| do { |
| if (ts.offset() < caretRowStartOffset) { |
| return false; |
| } |
| JsTokenId id = ts.token().id(); |
| switch (id) { |
| case WHITESPACE: |
| case LINE_COMMENT: |
| break; |
| case BLOCK_COMMENT: |
| case DOC_COMMENT: |
| if (first && caretOffset > ts.offset() && caretOffset < ts.offset() + ts.token().length()) { |
| // Caret contained within block comment -> do not add anything |
| return false; |
| } |
| break; // Skip |
| case BRACKET_LEFT_CURLY: |
| return !balancedAfter; |
| default: |
| return false; |
| } |
| first = false; |
| } while (ts.movePrevious()); |
| return false; |
| } |
| |
| /** |
| * From Java. |
| * |
| * Returns position of the first unpaired closing paren/brace/bracket from the caretOffset |
| * till the end of caret row. If there is no such element, position after the last non-white |
| * character on the caret row is returned. |
| */ |
| private int getRowOrBlockEnd(BaseDocument doc, int caretOffset, boolean[] insert) throws BadLocationException { |
| int rowEnd = org.netbeans.editor.Utilities.getRowLastNonWhite(doc, caretOffset); |
| if (rowEnd == -1 || caretOffset >= rowEnd) { |
| return caretOffset; |
| } |
| rowEnd += 1; |
| int parenBalance = 0; |
| int braceBalance = 0; |
| int bracketBalance = 0; |
| TokenSequence<? extends JsTokenId> ts = LexUtilities.getPositionedSequence( |
| doc, caretOffset, language); |
| if (ts == null) { |
| return caretOffset; |
| } |
| while (ts.offset() < rowEnd) { |
| JsTokenId id = ts.token().id(); |
| switch (id) { |
| case OPERATOR_SEMICOLON: |
| return ts.offset() + 1; |
| case OPERATOR_COMMA: |
| return ts.offset(); |
| case BRACKET_LEFT_PAREN: |
| parenBalance++; |
| break; |
| case BRACKET_RIGHT_PAREN: |
| if (parenBalance-- == 0) { |
| return ts.offset(); |
| } |
| break; |
| case BRACKET_LEFT_CURLY: |
| braceBalance++; |
| break; |
| case BRACKET_RIGHT_CURLY: |
| if (braceBalance-- == 0) { |
| return ts.offset(); |
| } |
| break; |
| case BRACKET_LEFT_BRACKET: |
| bracketBalance++; |
| break; |
| case BRACKET_RIGHT_BRACKET: |
| if (bracketBalance-- == 0) { |
| return ts.offset(); |
| } |
| break; |
| } |
| if (!ts.moveNext()) { |
| // this might happen in embedded case - line is not at the end |
| // but there are no more tokens - for example <script>function foo() {</script> |
| if ((caretOffset - ts.offset()) == 1 |
| && (bracketBalance == 1 || parenBalance == 1 || braceBalance == 1)) { |
| return caretOffset; |
| } |
| break; |
| } |
| } |
| |
| insert[0] = false; |
| return rowEnd; |
| } |
| |
| private int getUnbalancedCurlyOffset(BaseDocument doc, int offset) throws BadLocationException { |
| TokenSequence<? extends JsTokenId> ts = LexUtilities.getPositionedSequence( |
| doc, offset, language); |
| if (ts == null) { |
| return -1; |
| } |
| |
| int balance = 0; |
| while (ts.movePrevious()) { |
| Token t = ts.token(); |
| |
| if (t.id() == JsTokenId.BRACKET_RIGHT_CURLY) { |
| balance++; |
| } else if (t.id() == JsTokenId.BRACKET_LEFT_CURLY) { |
| balance--; |
| if (balance < 0) { |
| return ts.offset(); |
| } |
| } |
| } |
| return -1; |
| } |
| |
| private int getCurlyIndent(BaseDocument doc, int offset) { |
| try { |
| int lineStart = Utilities.getRowStart(doc, offset, 0); |
| TokenSequence<? extends JsTokenId> ts = LexUtilities.getTokenSequence( |
| doc, lineStart, language); |
| |
| int prevLineStart = -1; |
| if (ts != null) { |
| do { |
| ts.move(lineStart); |
| if (!ts.moveNext()) { |
| if (prevLineStart >= 0) { |
| return IndentUtils.lineIndent(doc, lineStart); |
| } else { |
| return GsfUtilities.getLineIndent(doc, offset); |
| } |
| } |
| |
| Token<? extends JsTokenId> token = LexUtilities.findNextNonWsNonComment(ts); |
| Token<? extends JsTokenId> nextToken = LexUtilities.findNextNonWsNonComment(ts); |
| if (!LexUtilities.isBinaryOperator(token.id(), nextToken.id())) { |
| ts.move(lineStart); |
| if (!ts.movePrevious()) { |
| return IndentUtils.lineIndent(doc, lineStart); |
| } |
| nextToken = token; |
| token = LexUtilities.findPreviousNonWsNonComment(ts); |
| if (!LexUtilities.isBinaryOperator(token.id(), nextToken.id())) { |
| return IndentUtils.lineIndent(doc, lineStart); |
| } |
| } |
| prevLineStart = lineStart; |
| lineStart = Utilities.getRowStart(doc, lineStart, -1); |
| } while (lineStart > 0); |
| |
| if (lineStart <= 0) { |
| return IndentUtils.lineIndent(doc, lineStart); |
| } |
| } |
| } catch (BadLocationException ex) { |
| LOGGER.log(Level.INFO, null, ex); |
| } |
| return GsfUtilities.getLineIndent(doc, offset); |
| } |
| |
| private boolean isDocToken(JsTokenId id) { |
| return id == JsTokenId.BLOCK_COMMENT || id == JsTokenId.DOC_COMMENT; |
| } |
| |
| private static boolean hasCommentEnd(TokenSequence ts) { |
| while (ts.moveNext()) { |
| Token<JsDocumentationTokenId> token = ts.token(); |
| if (token.id() == JsDocumentationTokenId.COMMENT_END) { |
| return true; |
| } else if (CharSequenceUtilities.endsWith(token.text(), "/")) { //NOI18N |
| if (ts.moveNext()) { |
| Token<JsDocumentationTokenId> nextToken = ts.token(); |
| if (CharSequenceUtilities.textEquals(nextToken.text(), "/")) { //NOI18N |
| ts.movePrevious(); |
| continue; |
| } else if (nextToken.id() == JsDocumentationTokenId.ASTERISK) { |
| return false; |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Return true iff the line for the given offset is a JavaScript comment line. |
| * This will return false for lines that contain comments (even when the |
| * offset is within the comment portion) but also contain code. |
| */ |
| private static boolean isCommentOnlyLine(BaseDocument doc, int offset, Language<JsTokenId> language) |
| throws BadLocationException { |
| |
| int begin = Utilities.getRowFirstNonWhite(doc, offset); |
| |
| if (begin == -1) { |
| return false; // whitespace only |
| } |
| |
| Token<? extends JsTokenId> token = LexUtilities.getToken(doc, begin, language); |
| if (token != null) { |
| return token.id() == JsTokenId.LINE_COMMENT; |
| } |
| |
| return false; |
| } |
| |
| /** Compute the balance of begin/end tokens on the line */ |
| private static int getLineBalance(BaseDocument doc, int offset, TokenId up, TokenId down) { |
| try { |
| int begin = Utilities.getRowStart(doc, offset); |
| int end = Utilities.getRowEnd(doc, offset); |
| |
| TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(doc, begin); |
| if (ts == null) { |
| return 0; |
| } |
| |
| ts.move(begin); |
| |
| if (!ts.moveNext()) { |
| return 0; |
| } |
| |
| int balance = 0; |
| |
| do { |
| Token<? extends JsTokenId> token = ts.token(); |
| TokenId id = token.id(); |
| |
| if (id == up) { |
| balance++; |
| } else if (id == down) { |
| balance--; |
| } |
| } while (ts.moveNext() && (ts.offset() <= end)); |
| |
| return balance; |
| } catch (BadLocationException ble) { |
| Exceptions.printStackTrace(ble); |
| |
| return 0; |
| } |
| } |
| |
| @MimeRegistrations({ |
| @MimeRegistration(mimeType = JsTokenId.JAVASCRIPT_MIME_TYPE, service = TypedBreakInterceptor.Factory.class), |
| @MimeRegistration(mimeType = JsDocumentationTokenId.MIME_TYPE, service = TypedBreakInterceptor.Factory.class) |
| }) |
| public static class JsFactory implements TypedBreakInterceptor.Factory { |
| |
| @Override |
| public TypedBreakInterceptor createTypedBreakInterceptor(MimePath mimePath) { |
| return new JsTypedBreakInterceptor(JsTokenId.javascriptLanguage(), true, true); |
| } |
| |
| } |
| |
| @MimeRegistration(mimeType = JsTokenId.JSON_MIME_TYPE, service = TypedBreakInterceptor.Factory.class) |
| public static class JsonFactory implements TypedBreakInterceptor.Factory { |
| |
| @Override |
| public TypedBreakInterceptor createTypedBreakInterceptor(MimePath mimePath) { |
| return new JsTypedBreakInterceptor( |
| JsTokenId.jsonLanguage(), |
| (doc) -> { |
| return Optional.ofNullable(EditorDocumentUtils.getFileObject(doc)) |
| .map((fo) -> JsonOptionsQuery.getOptions(fo).isCommentSupported()) |
| .orElse(false); |
| }, |
| false); |
| } |
| |
| } |
| |
| private static class CommentGenerator { |
| |
| private final int offset; |
| private final int indent; |
| |
| public CommentGenerator(int offset, int indent) { |
| this.offset = offset; |
| this.indent = indent; |
| } |
| |
| public int getIndent() { |
| return indent; |
| } |
| |
| public int getOffset() { |
| return offset; |
| } |
| |
| } |
| } |