blob: 7d5dd608721615a645c1c985d8c5cdb8a016b6d7 [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.groovy.editor.typinghooks;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.netbeans.api.editor.mimelookup.MimePath;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.Utilities;
import org.netbeans.modules.csl.spi.GsfUtilities;
import org.netbeans.modules.editor.indent.api.IndentUtils;
import org.netbeans.modules.groovy.editor.api.lexer.GroovyTokenId;
import org.netbeans.modules.groovy.editor.api.lexer.LexUtilities;
import org.netbeans.spi.editor.typinghooks.TypedBreakInterceptor;
/**
*
* @author Martin Janicek
*/
public class GroovyTypedBreakInterceptor implements TypedBreakInterceptor {
@MimeRegistration(mimeType = GroovyTokenId.GROOVY_MIME_TYPE, service = TypedBreakInterceptor.Factory.class)
public static class GroovyTypedBreakInterceptorFactory implements TypedBreakInterceptor.Factory {
@Override
public TypedBreakInterceptor createTypedBreakInterceptor(MimePath mimePath) {
return new GroovyTypedBreakInterceptor();
}
}
/**
* 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("groovy.cont.comment"); // NOI18N
@Override
public void insert(MutableContext context) throws BadLocationException {
BaseDocument doc = (BaseDocument) context.getDocument();
TokenHierarchy<Document> tokenHierarchy = TokenHierarchy.get(context.getDocument());
int offset = context.getCaretOffset();
boolean insertMatching = TypingHooksUtil.isMatchingBracketsEnabled(doc);
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<GroovyTokenId> ts = LexUtilities.getGroovyTokenSequence(tokenHierarchy, offset);
if (ts == null) {
return;
}
ts.move(offset);
if (!ts.moveNext() && !ts.movePrevious()) {
return;
}
Token<GroovyTokenId> token = ts.token();
GroovyTokenId id = token.id();
// Insert a missing }
boolean insertRightBrace = isAddRightBrace(doc, offset);
if (id != GroovyTokenId.ERROR && id != GroovyTokenId.BLOCK_COMMENT && insertMatching && insertRightBrace) {
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, GsfUtilities.getLineIndent(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, GsfUtilities.getLineIndent(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 == GroovyTokenId.ERROR) {
// See if it's a block comment opener
String text = token.text().toString();
if (text.startsWith("/*") && ts.offset() == Utilities.getRowFirstNonWhite(doc, offset)) {
int indent = GsfUtilities.getLineIndent(doc, 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
context.setText(sb.toString(), 0, carretOffset);
return;
}
}
if (id == GroovyTokenId.STRING_LITERAL || id == GroovyTokenId.STRING_CH && offset < ts.offset()+ts.token().length()) {
String tokenText = token.text().toString();
if (id == GroovyTokenId.STRING_LITERAL
&& (tokenText.startsWith("\"\"\"") || tokenText.startsWith("'''"))) {
return;
}
String str = (id != GroovyTokenId.STRING_LITERAL || offset > ts.offset()) ? "\\n\\\n" : "\\\n";
context.setText(str, -1, str.length());
return;
}
if (id == GroovyTokenId.REGEXP_LITERAL && 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 != GroovyTokenId.REGEXP_LITERAL || offset > ts.offset()) ? "\\n\\\n" : "\\\n";
context.setText(str, -1, str.length());
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 == GroovyTokenId.RBRACE || id == GroovyTokenId.RBRACKET) && offset > 0) {
Token<GroovyTokenId> prevToken = LexUtilities.getToken(doc, offset - 1);
if (prevToken != null) {
GroovyTokenId prevTokenId = prevToken.id();
if (id == GroovyTokenId.RBRACE && prevTokenId == GroovyTokenId.LBRACE ||
id == GroovyTokenId.RBRACKET && prevTokenId == GroovyTokenId.LBRACKET) {
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 (id == GroovyTokenId.WHITESPACE || id == GroovyTokenId.NLS) {
// 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 == GroovyTokenId.LINE_COMMENT) {
offset = begin;
}
}
}
}
if ((id == GroovyTokenId.BLOCK_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();
} 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 == GroovyTokenId.LINE_COMMENT;
if (id == GroovyTokenId.EOL) {
if (ts.movePrevious() && ts.token().id() == GroovyTokenId.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<GroovyTokenId> firstToken = LexUtilities.getToken(doc, prevBegin);
if (firstToken != null && firstToken.id() == GroovyTokenId.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<GroovyTokenId> firstToken = LexUtilities.getToken(doc, nextBegin);
if (firstToken != null && firstToken.id() == GroovyTokenId.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<GroovyTokenId> firstToken = LexUtilities.getToken(doc, begin);
if (firstToken.id() == GroovyTokenId.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<GroovyTokenId> firstToken = LexUtilities.getToken(doc, nextLineFirst);
if (firstToken != null && firstToken.id() == GroovyTokenId.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 {
}
@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)
&& !LexUtilities.isCommentOnlyLine(doc, currentOffset)) {
indent = GsfUtilities.getLineIndent(doc, currentOffset);
int parenBalance = LexUtilities.getLineBalance(doc, currentOffset,
GroovyTokenId.LPAREN, GroovyTokenId.RPAREN);
if (parenBalance < 0) {
break;
}
int curlyBalance = LexUtilities.getLineBalance(doc, currentOffset,
GroovyTokenId.LBRACE, GroovyTokenId.RBRACE);
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 {
if (LexUtilities.getTokenBalance(doc, GroovyTokenId.LBRACE, GroovyTokenId.RBRACE, caretOffset) <= 0) {
return false;
}
int caretRowStartOffset = org.netbeans.editor.Utilities.getRowStart(doc, caretOffset);
TokenSequence<GroovyTokenId> ts = LexUtilities.getPositionedSequence(doc, caretOffset);
if (ts == null) {
return false;
}
boolean first = true;
do {
if (ts.offset() < caretRowStartOffset) {
return false;
}
GroovyTokenId id = ts.token().id();
switch (id) {
case WHITESPACE:
case LINE_COMMENT:
break;
case BLOCK_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 LBRACE:
return true;
}
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<GroovyTokenId> ts = LexUtilities.getPositionedSequence(doc, caretOffset);
if (ts == null) {
return caretOffset;
}
while (ts.offset() < rowEnd) {
GroovyTokenId id = ts.token().id();
switch (id) {
case SEMI:
return ts.offset() + 1;
case COMMA:
return ts.offset();
case LPAREN:
parenBalance++;
break;
case RPAREN:
if (parenBalance-- == 0) {
return ts.offset();
}
break;
case LBRACE:
braceBalance++;
break;
case RBRACE:
if (braceBalance-- == 0) {
return ts.offset();
}
break;
case LBRACKET:
bracketBalance++;
break;
case RBRACKET:
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<GroovyTokenId> ts = LexUtilities.getPositionedSequence(doc, offset);
if (ts == null) {
return -1;
}
int balance = 0;
while (ts.movePrevious()) {
Token t = ts.token();
if (t.id() == GroovyTokenId.RBRACE) {
balance++;
} else if (t.id() == GroovyTokenId.LBRACE) {
balance--;
if (balance < 0) {
return ts.offset();
}
}
}
return -1;
}
}