blob: f186ecb4b6574f3b3ae8ce93e3086ddd226d8d4e [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.php.editor.indent;
import java.util.Arrays;
import java.util.Collection;
import javax.swing.text.BadLocationException;
import org.netbeans.api.editor.document.LineDocumentUtils;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenId;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.lexer.TokenUtilities;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.Utilities;
import org.netbeans.modules.editor.indent.spi.Context;
import org.netbeans.modules.php.editor.lexer.LexUtilities;
import org.netbeans.modules.php.editor.lexer.PHPTokenId;
import org.openide.util.Exceptions;
/**
* Extracted from Tomasz Slota's PHPNewLineIndenter.
*
* @author Ondrej Brejla <obrejla@netbeans.org>
*/
public class IndentationCounter {
private static final Collection<PHPTokenId> CONTROL_STATEMENT_TOKENS = Arrays.asList(
PHPTokenId.PHP_DO, PHPTokenId.PHP_WHILE, PHPTokenId.PHP_FOR,
PHPTokenId.PHP_FOREACH, PHPTokenId.PHP_IF, PHPTokenId.PHP_ELSE);
private Collection<ScopeDelimiter> scopeDelimiters;
private final BaseDocument doc;
private final int indentSize;
private final int continuationSize;
private final int itemsArrayDeclararionSize;
public IndentationCounter(BaseDocument doc) {
this.doc = doc;
indentSize = CodeStyle.get(doc).getIndentSize();
continuationSize = CodeStyle.get(doc).getContinuationIndentSize();
itemsArrayDeclararionSize = CodeStyle.get(doc).getItemsInArrayDeclarationIndentSize();
int initialIndentSize = CodeStyle.get(doc).getInitialIndent();
scopeDelimiters = Arrays.asList(
new ScopeDelimiter(PHPTokenId.PHP_SEMICOLON, 0),
new ScopeDelimiter(PHPTokenId.PHP_OPENTAG, initialIndentSize),
new ScopeDelimiter(PHPTokenId.PHP_CURLY_CLOSE, 0),
new ScopeDelimiter(PHPTokenId.PHP_CURLY_OPEN, indentSize),
new ScopeDelimiter(PHPTokenId.PHP_CASE, indentSize),
new ScopeDelimiter(PHPTokenId.PHP_IF, indentSize),
new ScopeDelimiter(PHPTokenId.PHP_ELSE, indentSize),
new ScopeDelimiter(PHPTokenId.PHP_ELSEIF, indentSize),
new ScopeDelimiter(PHPTokenId.PHP_WHILE, indentSize),
new ScopeDelimiter(PHPTokenId.PHP_DO, indentSize),
new ScopeDelimiter(PHPTokenId.PHP_FOR, indentSize),
new ScopeDelimiter(PHPTokenId.PHP_FOREACH, indentSize),
new ScopeDelimiter(PHPTokenId.PHP_DEFAULT, indentSize));
}
public Indentation count(int caretOffset) {
Indentation result = Indentation.NONE;
doc.readLock();
try {
result = countUnderReadLock(caretOffset);
} finally {
doc.readUnlock();
}
return result;
}
private Indentation countUnderReadLock(int caretOffset) {
int newIndent = 0;
try {
boolean insideString = false;
TokenSequence<? extends PHPTokenId> ts = LexUtilities.getPHPTokenSequence(doc, caretOffset);
int caretLineStart = LineDocumentUtils.getLineStart(doc, LineDocumentUtils.getLineStart(doc, caretOffset) - 1);
if (ts != null) {
ts.move(caretOffset);
ts.moveNext();
boolean indentStartComment = false;
boolean movePrevious = false;
if (ts.token() == null) {
return Indentation.NONE;
}
if (ts.token().id() == PHPTokenId.PHP_OPENTAG) {
int neOffset = LineDocumentUtils.getPreviousNonWhitespace(doc, caretOffset - 1);
Indentation result = Indentation.NONE;
if (neOffset != -1) {
result = new IndentationImpl(Utilities.getRowIndent(doc, neOffset) + indentSize);
}
return result;
}
if (ts.token().id() == PHPTokenId.WHITESPACE && ts.moveNext()) {
movePrevious = true;
}
// #268621
if (ts.token().id() == PHPTokenId.PHP_CURLY_OPEN) {
newIndent = Utilities.getRowIndent(doc, caretLineStart);
if (newIndent < 0) {
int caretStart = caretOffset - 1;
int caretLineEnd = LineDocumentUtils.getLineEnd(doc, LineDocumentUtils.getLineEnd(doc, caretOffset) - 1);
int curlyOffset = ts.offset() - 1;
if (caretLineEnd == caretStart) {
newIndent = caretStart - caretLineStart;
} else if (caretLineEnd < curlyOffset) {
// -1 : in this case, caretLineEnd is the top of the next line
newIndent = caretLineEnd - 1 - caretLineStart;
} else {
newIndent = curlyOffset - caretLineStart;
}
if (newIndent < 0) {
newIndent = 0;
}
}
return new IndentationImpl(newIndent);
}
if (ts.token().id() == PHPTokenId.PHP_COMMENT
|| ts.token().id() == PHPTokenId.PHP_LINE_COMMENT
|| ts.token().id() == PHPTokenId.PHP_COMMENT_START
|| ts.token().id() == PHPTokenId.PHP_COMMENT_END) {
if (ts.token().id() == PHPTokenId.PHP_COMMENT_START && ts.offset() >= caretOffset) {
indentStartComment = true;
} else {
if (!movePrevious) {
// don't indent comment - issue #173979
return Indentation.NONE;
} else {
if (ts.token().id() == PHPTokenId.PHP_LINE_COMMENT) {
ts.movePrevious();
CharSequence whitespace = ts.token().text();
if (ts.movePrevious() && ts.token().id() == PHPTokenId.PHP_LINE_COMMENT) {
int index = 0;
while (index < whitespace.length() && whitespace.charAt(index) != '\n') {
index++;
}
if (index == whitespace.length()) {
// don't indent if the line commnet continue
// the last new line belongs to the line comment
return Indentation.NONE;
}
}
ts.moveNext();
movePrevious = false;
}
}
}
}
if (movePrevious) {
ts.movePrevious();
}
if ((ts.token().id() == PHPTokenId.PHP_ENCAPSED_AND_WHITESPACE || ts.token().id() == PHPTokenId.PHP_CONSTANT_ENCAPSED_STRING) && caretOffset > ts.offset()) {
int stringLineStart = LineDocumentUtils.getLineStart(doc, ts.offset());
if (stringLineStart >= caretLineStart) {
// string starts on the same line:
// current line indent + continuation size
newIndent = Utilities.getRowIndent(doc, stringLineStart) + indentSize;
} else {
// string starts before:
// repeat indent from the previous line
newIndent = Utilities.getRowIndent(doc, caretLineStart);
}
insideString = true;
}
int bracketBalance = 0;
int squaredBalance = 0;
PHPTokenId previousTokenId = ts.token().id();
while (!insideString && ts.movePrevious()) {
Token token = ts.token();
ScopeDelimiter delimiter = getScopeDelimiter(token);
int anchor = ts.offset();
int shiftAtAncor = 0;
if (delimiter != null) {
if (delimiter.tokenId == PHPTokenId.PHP_SEMICOLON) {
int casePosition = breakProceededByCase(ts); // is after break in case statement?
if (casePosition > -1) {
newIndent = Utilities.getRowIndent(doc, anchor);
if (LineDocumentUtils.getLineStart(doc, casePosition) != caretLineStart) {
// check that case is not on the same line, where enter was pressed
newIndent -= indentSize;
}
break;
}
CodeB4BreakData codeB4BreakData = processCodeBeforeBreak(ts, indentStartComment);
anchor = codeB4BreakData.expressionStartOffset;
shiftAtAncor = codeB4BreakData.indentDelta;
if (codeB4BreakData.processedByControlStmt) {
newIndent = Utilities.getRowIndent(doc, anchor) - indentSize;
} else {
newIndent = Utilities.getRowIndent(doc, anchor) + delimiter.indentDelta + shiftAtAncor;
}
break;
} else if (delimiter.tokenId == PHPTokenId.PHP_CURLY_OPEN && ts.movePrevious()) {
int startExpression = LexUtilities.findStartTokenOfExpression(ts);
newIndent = Utilities.getRowIndent(doc, startExpression) + indentSize;
break;
}
if (anchor >= 0) {
newIndent = Utilities.getRowIndent(doc, anchor) + delimiter.indentDelta + shiftAtAncor;
}
break;
} else {
if (ts.token().id() == PHPTokenId.PHP_TOKEN
|| (ts.token().id() == PHPTokenId.PHP_OPERATOR && TokenUtilities.textEquals("=", ts.token().text()))) { // NOI18N
char ch = ts.token().text().charAt(0);
boolean continualIndent = false;
boolean indent = false;
switch (ch) {
case ')':
bracketBalance++;
break;
case '(':
if (bracketBalance == 0) {
continualIndent = true;
}
bracketBalance--;
break;
case ']':
squaredBalance++;
break;
case '[':
if (squaredBalance == 0) {
continualIndent = true;
}
squaredBalance--;
break;
case ',':
continualIndent = true;
break;
case '.':
continualIndent = true;
break;
case ':':
if (isInTernaryOperatorStatement(ts)) {
continualIndent = true;
} else {
indent = true;
}
break;
case '=':
continualIndent = true;
break;
default:
//no-op
}
if (continualIndent || indent) {
ts.move(caretOffset);
ts.movePrevious();
int startExpression = LexUtilities.findStartTokenOfExpression(ts);
if (startExpression != -1) {
if (continualIndent) {
int offsetArrayDeclaration = offsetArrayDeclaration(startExpression, ts);
if (offsetArrayDeclaration > -1) {
newIndent = Utilities.getRowIndent(doc, offsetArrayDeclaration) + itemsArrayDeclararionSize;
} else if (inGroupUse(startExpression, ts)) {
newIndent = Utilities.getRowIndent(doc, startExpression);
} else {
newIndent = Utilities.getRowIndent(doc, startExpression) + continuationSize;
}
}
if (indent) {
newIndent = Utilities.getRowIndent(doc, startExpression) + indentSize;
}
}
break;
}
} else if ((previousTokenId == PHPTokenId.PHP_OBJECT_OPERATOR
|| ts.token().id() == PHPTokenId.PHP_OBJECT_OPERATOR
|| ts.token().id() == PHPTokenId.PHP_PAAMAYIM_NEKUDOTAYIM) && bracketBalance <= 0) {
int startExpression = LexUtilities.findStartTokenOfExpression(ts);
if (startExpression != -1) {
int rememberOffset = ts.offset();
ts.move(startExpression);
ts.moveNext();
if (ts.token().id() != PHPTokenId.PHP_IF
&& ts.token().id() != PHPTokenId.PHP_WHILE
&& ts.token().id() != PHPTokenId.PHP_FOR
&& ts.token().id() != PHPTokenId.PHP_FOREACH) {
newIndent = Utilities.getRowIndent(doc, startExpression) + continuationSize;
break;
} else {
ts.move(rememberOffset);
ts.moveNext();
}
}
} else if (ts.token().id() == PHPTokenId.PHP_PUBLIC || ts.token().id() == PHPTokenId.PHP_PROTECTED
|| ts.token().id() == PHPTokenId.PHP_PRIVATE || (ts.token().id() == PHPTokenId.PHP_VARIABLE && bracketBalance <= 0)) {
int startExpression = LexUtilities.findStartTokenOfExpression(ts);
if (startExpression != -1) {
newIndent = Utilities.getRowIndent(doc, startExpression) + continuationSize;
break;
}
}
}
previousTokenId = ts.token().id();
}
if (newIndent < 0) {
newIndent = 0;
}
}
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
}
return new IndentationImpl(newIndent);
}
private static boolean isInTernaryOperatorStatement(TokenSequence<? extends PHPTokenId> ts) {
boolean result = false;
int originalOffset = ts.offset();
ts.movePrevious();
Token<? extends PHPTokenId> previousToken = LexUtilities.findPreviousToken(ts, Arrays.asList(PHPTokenId.PHP_TOKEN));
if (previousToken != null && previousToken.id() == PHPTokenId.PHP_TOKEN && previousToken.text().charAt(0) == '?') {
result = true;
}
ts.move(originalOffset);
ts.moveNext();
return result;
}
private CodeB4BreakData processCodeBeforeBreak(TokenSequence ts, boolean indentComment) {
CodeB4BreakData retunValue = new CodeB4BreakData();
int origOffset = ts.offset();
Token token = ts.token();
if (token.id() == PHPTokenId.PHP_SEMICOLON && ts.movePrevious()) {
retunValue.expressionStartOffset = LexUtilities.findStartTokenOfExpression(ts);
ts.move(retunValue.expressionStartOffset);
ts.moveNext();
retunValue.indentDelta = ts.token().id() == PHPTokenId.PHP_CASE || ts.token().id() == PHPTokenId.PHP_DEFAULT
? indentSize : 0;
retunValue.processedByControlStmt = false;
ts.move(origOffset);
ts.moveNext();
return retunValue;
}
while (ts.movePrevious()) {
token = ts.token();
ScopeDelimiter delimiter = getScopeDelimiter(token);
if (delimiter != null) {
retunValue.expressionStartOffset = ts.offset();
retunValue.indentDelta = delimiter.indentDelta;
if (CONTROL_STATEMENT_TOKENS.contains(delimiter.tokenId)) {
retunValue.indentDelta = 0;
}
break;
} else {
if (indentComment && token.id() == PHPTokenId.WHITESPACE
&& TokenUtilities.indexOf(token.text(), '\n') != -1
&& ts.moveNext()) {
retunValue.expressionStartOffset = ts.offset();
retunValue.indentDelta = 0;
break;
}
}
}
if (token.id() == PHPTokenId.PHP_OPENTAG && ts.moveNext()) {
// we are at the begining of the php blog
LexUtilities.findNext(ts, Arrays.asList(
PHPTokenId.WHITESPACE,
PHPTokenId.PHPDOC_COMMENT, PHPTokenId.PHPDOC_COMMENT_END, PHPTokenId.PHPDOC_COMMENT_START,
PHPTokenId.PHP_COMMENT, PHPTokenId.PHP_COMMENT_END, PHPTokenId.PHP_COMMENT_START,
PHPTokenId.PHP_LINE_COMMENT));
retunValue.expressionStartOffset = ts.offset();
retunValue.indentDelta = 0;
}
ts.move(origOffset);
ts.moveNext();
return retunValue;
}
/**
* Returns of set of the array declaration, where is the exexpression.
*
* @param startExpression
* @param ts
* @return
*/
private static int offsetArrayDeclaration(int startExpression, TokenSequence ts) {
int result = -1;
int origOffset = ts.offset();
Token token;
int balance = 0;
int squaredBalance = 0;
do {
token = ts.token();
if (token.id() == PHPTokenId.PHP_TOKEN) {
switch (token.text().charAt(0)) {
case ')':
balance--;
break;
case '(':
balance++;
break;
case ']':
squaredBalance--;
break;
case '[':
squaredBalance++;
break;
default:
//no-op
}
}
} while (ts.offset() > startExpression
&& !(token.id() == PHPTokenId.PHP_ARRAY && balance == 1)
&& !(token.id() == PHPTokenId.PHP_TOKEN && squaredBalance == 1)
&& ts.movePrevious());
if ((token.id() == PHPTokenId.PHP_ARRAY && balance == 1)
|| (token.id() == PHPTokenId.PHP_TOKEN && squaredBalance == 1)) {
result = ts.offset();
}
ts.move(origOffset);
ts.moveNext();
return result;
}
private boolean inGroupUse(int startExpression, TokenSequence ts) {
boolean result = false;
int origOffset = ts.offset();
ts.move(startExpression);
// move to start expression
if (ts.moveNext()
&& ts.movePrevious()) {
// try to find '{', namespace and then 'use' (possibly with 'const' or 'function')
boolean openCurlyFound = false;
boolean namespaceFound = false;
for (;;) {
TokenId tokenId = ts.token().id();
if (tokenId == PHPTokenId.PHP_USE) {
result = openCurlyFound && namespaceFound;
break;
} else if (tokenId == PHPTokenId.PHP_CURLY_OPEN) {
if (openCurlyFound) {
break;
}
openCurlyFound = true;
} else if (tokenId == PHPTokenId.PHP_NS_SEPARATOR) {
namespaceFound = true;
} else if (tokenId != PHPTokenId.WHITESPACE
&& tokenId != PHPTokenId.PHP_STRING
&& tokenId != PHPTokenId.PHP_CONST
&& tokenId != PHPTokenId.PHP_FUNCTION) {
break;
}
if (!ts.movePrevious()) {
break;
}
}
}
ts.move(origOffset);
ts.moveNext();
return result;
}
/**
*
* @param ts
* @return -1 if is not by case or offset of the case keyword
*/
private int breakProceededByCase(TokenSequence<? extends PHPTokenId> ts) {
int retunValue = -1;
int origOffset = ts.offset();
if (ts.movePrevious()) {
if (semicolonProceededByBreak(ts)) {
while (ts.movePrevious()) {
PHPTokenId tid = ts.token().id();
if (tid == PHPTokenId.PHP_CASE) {
retunValue = ts.offset();
break;
} else if (CONTROL_STATEMENT_TOKENS.contains(tid)) {
break;
}
}
}
}
ts.move(origOffset);
ts.moveNext();
return retunValue;
}
private boolean semicolonProceededByBreak(TokenSequence ts) {
boolean retunValue = false;
if (ts.token().id() == PHPTokenId.PHP_BREAK) {
retunValue = true;
} else if (ts.token().id() == PHPTokenId.PHP_NUMBER) {
int origOffset = ts.offset();
if (ts.movePrevious()) {
if (ts.token().id() == PHPTokenId.WHITESPACE) {
if (ts.movePrevious()) {
if (ts.token().id() == PHPTokenId.PHP_BREAK) {
retunValue = true;
}
}
}
}
ts.move(origOffset);
ts.moveNext();
}
return retunValue;
}
private ScopeDelimiter getScopeDelimiter(Token token) {
// TODO: more efficient impl
for (ScopeDelimiter scopeDelimiter : scopeDelimiters) {
if (scopeDelimiter.matches(token)) {
return scopeDelimiter;
}
}
return null;
}
private static class CodeB4BreakData {
int expressionStartOffset;
boolean processedByControlStmt;
int indentDelta;
}
private static class ScopeDelimiter {
private PHPTokenId tokenId;
private String tokenContent;
private int indentDelta;
public ScopeDelimiter(PHPTokenId tokenId, int indentDelta) {
this(tokenId, null, indentDelta);
}
public ScopeDelimiter(PHPTokenId tokenId, String tokenContent, int indentDelta) {
this.tokenId = tokenId;
this.tokenContent = tokenContent;
this.indentDelta = indentDelta;
}
public boolean matches(Token token) {
if (tokenId != token.id()) {
return false;
}
if (tokenContent != null
&& TokenUtilities.textEquals(token.text(), tokenContent)) {
return false;
}
return true;
}
}
public interface Indentation {
Indentation NONE = new Indentation() {
@Override
public int getIndentation() {
return 0;
}
@Override
public void modify(Context context) {
}
};
int getIndentation();
void modify(Context context);
}
private static final class IndentationImpl implements Indentation {
private final int indentation;
public IndentationImpl(int indentation) {
this.indentation = indentation;
}
@Override
public int getIndentation() {
return indentation;
}
@Override
public void modify(final Context context) {
assert context != null;
context.document().render(new Runnable() {
@Override
public void run() {
modifyUnderWriteLock(context);
}
});
}
private void modifyUnderWriteLock(Context context) {
try {
context.modifyIndent(LineDocumentUtils.getLineStart((BaseDocument) context.document(), context.caretOffset()), indentation);
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
}
}
}
}