blob: e6069f8b2ec7694f24d19f1cbd119e6e1fade172 [file] [log] [blame]
/*
* 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;
}
}
}