/*******************************************************************************
* Copyright (c) 2014, 2016 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* Zend Technologies
*******************************************************************************/
package org.eclipse.php.internal.core.format;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.php.internal.core.documentModel.parser.regions.IPHPScriptRegion;
import org.eclipse.php.internal.core.documentModel.parser.regions.PHPRegionTypes;
import org.eclipse.php.internal.core.documentModel.partitioner.PHPPartitionTypes;
import org.eclipse.php.internal.core.util.text.PHPTextSequenceUtilities;
import org.eclipse.wst.sse.core.internal.parser.ContextRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionContainer;
public class IndentationBaseDetector {
private IStructuredDocument document;
private int currLineIndex;
private int offset;
private Region textSequenceRegion;
public IndentationBaseDetector(IStructuredDocument document, int currLineIndex, final int offset)
throws BadLocationException {
this.document = document;
this.currLineIndex = currLineIndex;
this.offset = offset;
IRegion lineInfo = document.getLineInformation(currLineIndex);
this.textSequenceRegion = PHPTextSequenceUtilities.getStatementRegion(lineInfo.getOffset(),
document.getRegionAtCharacterOffset(lineInfo.getOffset()), true);
}
public int getIndentationBaseLine(boolean checkMultiLineStatement) throws BadLocationException {
if (checkMultiLineStatement) {
currLineIndex = adjustLine(currLineIndex, offset);
}
while (currLineIndex > 0) {
if (isIndentationBase(offset, currLineIndex, checkMultiLineStatement)) {
return currLineIndex;
}
currLineIndex = getNextLineIndex(offset, currLineIndex, checkMultiLineStatement);
}
return 0;
}
private int adjustLine(int currLineIndex, int offset) throws BadLocationException {
// TODO ignore the comment
final IRegion lineInfo = document.getLineInformation(currLineIndex);
int lineEnd = lineInfo.getOffset() + lineInfo.getLength();
lineEnd = Math.min(lineEnd, offset);
if (lineEnd == lineInfo.getOffset()) {
lineEnd = IndentationUtils.moveLineStartToNonBlankChar(document, lineEnd, currLineIndex, false);
}
if (lineEnd == document.getLength() && lineEnd > 0) {
lineEnd--;
}
PHPHeuristicScanner scanner = PHPHeuristicScanner.createHeuristicScanner(document, lineEnd, true);
int token = scanner.previousToken(lineEnd, PHPHeuristicScanner.UNBOUND);
if (token == PHPHeuristicScanner.TokenSEMICOLON) {
token = scanner.previousToken(scanner.getPosition(), PHPHeuristicScanner.UNBOUND);
}
if (token == PHPHeuristicScanner.TokenRPAREN) {
int peer = scanner.findOpeningPeer(scanner.getPosition(), PHPHeuristicScanner.UNBOUND,
PHPHeuristicScanner.LPAREN, PHPHeuristicScanner.RPAREN);
if (peer != PHPHeuristicScanner.NOT_FOUND) {
return document.getLineOfOffset(scanner.getPosition());
}
} else if (token == PHPHeuristicScanner.TokenRBRACKET) {
int peer = scanner.findOpeningPeer(scanner.getPosition(), PHPHeuristicScanner.UNBOUND,
PHPHeuristicScanner.LBRACKET, PHPHeuristicScanner.RBRACKET);
if (peer != PHPHeuristicScanner.NOT_FOUND) {
return document.getLineOfOffset(scanner.getPosition());
}
}
return currLineIndex;
}
private boolean isIndentationBase(int offset, int currLineIndex, boolean checkMultiLineStatement)
throws BadLocationException {
final IRegion lineInfo = document.getLineInformation(currLineIndex);
if (lineInfo.getLength() == 0) {
return false;
}
final int checkedOffset = Math.min(lineInfo.getOffset() + lineInfo.getLength(), offset);
int lineStartOffset = lineInfo.getOffset();
if (IndentationUtils.isBlanks(document, lineStartOffset, checkedOffset)) {
return false;
}
PHPHeuristicScanner scanner = PHPHeuristicScanner.createHeuristicScanner(document, checkedOffset, true);
if (IndentationUtils.inBracelessBlock(scanner, document, checkedOffset)) {
return true;
}
// NB: lineStartOffset could be greater than checkedOffset after this
// loop
while (Character.isWhitespace(document.getChar(lineStartOffset))) {
lineStartOffset++;
}
// need to get to the first tRegion - so that we wont get the state of
// the tRegion in the previous line
// checked line beginning offset (after incrementing spaces in beginning
final String checkedLineBeginState = FormatterUtils.getPartitionType(document, lineStartOffset);
// checked line end
final String checkedLineEndState = FormatterUtils.getPartitionType(document, checkedOffset);
// the current potential line for formatting begin offset
final String forLineEndState = FormatterUtils.getPartitionType(document, offset);
if (isMultilineAfterBraceless(checkedOffset)) {
// braceless block end go up
return false;
}
boolean shouldNotConsiderAsIndentationBase = shouldNotConsiderAsIndentationBase(checkedLineBeginState,
forLineEndState);
if ((shouldNotConsiderAsIndentationBase || (checkMultiLineStatement && isInMultiLineStatement(currLineIndex)
&& !isMultilineContentInsideBraceless(checkedOffset)))
&& !lineContainIncompleteBlock(checkedOffset, lineStartOffset)) {
return false;
}
// Fix bug #201688
if (((checkedLineBeginState == PHPPartitionTypes.PHP_MULTI_LINE_COMMENT)
|| (checkedLineBeginState == PHPPartitionTypes.PHP_DOC))
&& (checkedLineBeginState == forLineEndState)) {
// the whole document
final IStructuredDocumentRegion sdRegion = document.getRegionAtCharacterOffset(lineStartOffset);
// the whole PHP script
ITextRegion phpScriptRegion = sdRegion.getRegionAtCharacterOffset(lineStartOffset);
int phpContentStartOffset = sdRegion.getStartOffset(phpScriptRegion);
if (phpScriptRegion instanceof ITextRegionContainer) {
ITextRegionContainer container = (ITextRegionContainer) phpScriptRegion;
phpScriptRegion = container.getRegionAtCharacterOffset(lineStartOffset);
phpContentStartOffset += phpScriptRegion.getStart();
}
if (phpScriptRegion instanceof IPHPScriptRegion) {
IPHPScriptRegion scriptRegion = (IPHPScriptRegion) phpScriptRegion;
// the region we are trying to check if it is the indent base
// for the line we need to format
ContextRegion checkedRegion = (ContextRegion) scriptRegion
.getPHPToken(lineStartOffset - phpContentStartOffset);
// the current region we need to format
ContextRegion currentRegion = (ContextRegion) scriptRegion.getPHPToken(offset - phpContentStartOffset);
String checkedType = checkedRegion.getType();
String currentType = currentRegion.getType();
// if we are in the beginning of a comment (DOC or Multi
// comment) and we have before another
// Doc comment or Multi comment, the base line we'll be the
// beginning of the previous multi comment
if (PHPPartitionTypes.isPHPDocStartRegion(currentType)
|| PHPPartitionTypes.isPHPMultiLineCommentStartRegion(currentType)) {
return PHPPartitionTypes.isPHPDocStartRegion(checkedType)
|| PHPPartitionTypes.isPHPMultiLineCommentStartRegion(checkedType);
}
}
}
return lineShouldIndent(checkedLineBeginState, checkedLineEndState) || forLineEndState == checkedLineBeginState;
}
private int getMultiLineStatementStartOffset(int currLineIndex) throws BadLocationException {
if (textSequenceRegion.getLength() != 0
&& IndentationUtils.isRegionTypeAllowedMultiline(
FormatterUtils.getRegionType(document, textSequenceRegion.getOffset()))
&& document.getLineOfOffset(textSequenceRegion.getOffset()) < currLineIndex) {
return document.getLineOfOffset(textSequenceRegion.getOffset());
}
return -1;
}
// NB: lineStartOffset can be greater than checkedOffset
private boolean lineContainIncompleteBlock(int checkedOffset, int lineStartOffset) throws BadLocationException {
if (checkedOffset == document.getLength() && checkedOffset > 0) {
checkedOffset--;
}
if (textSequenceRegion.getLength() != 0 && IndentationUtils
.isRegionTypeAllowedMultiline(FormatterUtils.getRegionType(document, textSequenceRegion.getOffset()))) {
PHPHeuristicScanner scanner = PHPHeuristicScanner.createHeuristicScanner(document, lineStartOffset, true);
// search for opening pear only in this line
int statementStart = document.getLineInformationOfOffset(checkedOffset).getOffset();
int openParenPeer = scanner.findOpeningPeer(checkedOffset - 1, statementStart, PHPHeuristicScanner.LPAREN,
PHPHeuristicScanner.RPAREN);
int bound = openParenPeer != -1 ? Math.max(statementStart, openParenPeer) : statementStart;
int openBracePeer = scanner.findOpeningPeer(checkedOffset - 1, bound, PHPHeuristicScanner.LBRACE,
PHPHeuristicScanner.RBRACE);
bound = openBracePeer != -1 || openParenPeer != -1
? Math.max(statementStart, Math.max(openParenPeer, openBracePeer)) : statementStart;
int openBracketPeer = scanner.findOpeningPeer(checkedOffset - 1, bound, PHPHeuristicScanner.LBRACKET,
PHPHeuristicScanner.RBRACKET);
int biggest = Math.max(openParenPeer, openBracePeer);
biggest = Math.max(biggest, openBracketPeer);
if (biggest != PHPHeuristicScanner.NOT_FOUND && biggest >= lineStartOffset) {
// the whole document
final IStructuredDocumentRegion sdRegion = document.getRegionAtCharacterOffset(lineStartOffset);
// the whole PHP script
ITextRegion tRegion = sdRegion.getRegionAtCharacterOffset(lineStartOffset);
int regionStart = sdRegion.getStartOffset(tRegion);
if (tRegion instanceof ITextRegionContainer) {
ITextRegionContainer container = (ITextRegionContainer) tRegion;
tRegion = container.getRegionAtCharacterOffset(lineStartOffset);
regionStart += tRegion.getStart();
}
if (tRegion instanceof IPHPScriptRegion) {
IPHPScriptRegion scriptRegion = (IPHPScriptRegion) tRegion;
ITextRegion[] tokens = null;
try {
tokens = scriptRegion.getPHPTokens(lineStartOffset - regionStart, biggest - lineStartOffset);
} catch (BadLocationException e) {
}
if (tokens != null && tokens.length > 0) {
Set<String> tokenTypeSet = new HashSet<String>();
for (int i = 0; i < tokens.length; i++) {
tokenTypeSet.add(tokens[i].getType());
}
if (biggest == openParenPeer) {
if (tokenTypeSet.contains(PHPRegionTypes.PHP_NEW)
|| tokenTypeSet.contains(PHPRegionTypes.PHP_FUNCTION)
|| tokenTypeSet.contains(PHPRegionTypes.PHP_ARRAY)) {
return true;
}
} else if (biggest == openBracePeer) {
if (tokenTypeSet.contains(PHPRegionTypes.PHP_NEW)
|| tokenTypeSet.contains(PHPRegionTypes.PHP_FUNCTION)) {
return true;
}
} else if (biggest == openBracketPeer && scanner.previousToken(biggest - 1,
PHPHeuristicScanner.UNBOUND) < PHPHeuristicScanner.TokenIDENT) {
return true;
} else {
if (tokenTypeSet.contains(PHPRegionTypes.PHP_ARRAY)) {
return true;
}
}
}
}
}
}
return false;
}
private int getNextLineIndex(int offset, int currLineIndex, boolean checkMultiLineStatement)
throws BadLocationException {
final IRegion lineInfo = document.getLineInformation(currLineIndex);
final int currLineEndOffset = lineInfo.getOffset() + lineInfo.getLength();
String checkedLineBeginState = FormatterUtils.getPartitionType(document, lineInfo.getOffset());
String forLineEndState = FormatterUtils.getPartitionType(document, currLineEndOffset);
int insideBraceless = getMultilineInsideBraceless(Math.min(currLineEndOffset, offset));
if (insideBraceless >= 0) {
return document.getLineOfOffset(insideBraceless);
}
if (isMultilineType(checkedLineBeginState) && (checkMultiLineStatement
|| shouldNotConsiderAsIndentationBase(checkedLineBeginState, forLineEndState))) {
int index = getMultiLineStatementStartOffset(lineInfo.getOffset(), currLineIndex);
if (index > -1) {
return index;
}
}
if (checkMultiLineStatement) {
int result = adjustLine(currLineIndex, currLineEndOffset);
if (result == currLineIndex && result != 0) {
result--;
}
return result;
}
return currLineIndex - 1;
}
private int getMultiLineStatementStartOffset(int lineStartOffset, int currLineIndex) {
if (textSequenceRegion.getLength() != 0) {
int textOriginalOffset = textSequenceRegion.getOffset();
int textSequenceLine = document.getLineOfOffset(textOriginalOffset);
if (textSequenceLine < currLineIndex && IndentationUtils
.isRegionTypeAllowedMultiline(FormatterUtils.getRegionType(document, textOriginalOffset))) {
return textSequenceLine;
}
}
return -1;
}
private int getMultilineInsideBraceless(int checkedOffset) {
try {
PHPHeuristicScanner scanner = PHPHeuristicScanner.createHeuristicScanner(document, checkedOffset - 1, true);
int start = scanner.previousToken(checkedOffset - 1, PHPHeuristicScanner.UNBOUND);
if (!(start == PHPHeuristicScanner.TokenRBRACE))
return -1;
int openingPeer = scanner.findOpeningPeer(scanner.getPosition(), PHPHeuristicScanner.UNBOUND,
PHPHeuristicScanner.LBRACE, PHPHeuristicScanner.RBRACE);
if (openingPeer == PHPHeuristicScanner.NOT_FOUND) {
return -1;
}
int prev = scanner.previousToken(openingPeer - 1, PHPHeuristicScanner.UNBOUND);
if (prev == PHPHeuristicScanner.TokenRPAREN) {
int openParent = scanner.findOpeningPeer(scanner.getPosition(), PHPHeuristicScanner.UNBOUND,
PHPHeuristicScanner.LPAREN, PHPHeuristicScanner.RPAREN);
if (openParent == PHPHeuristicScanner.NOT_FOUND) {
return -1;
}
prev = scanner.previousToken(openParent - 1, PHPHeuristicScanner.UNBOUND);
}
if (!IndentationUtils.inBracelessBlock(scanner, document, scanner.getPosition() - 1)) {
return -1;
}
// move to first from braceless
prev = scanner.previousToken(scanner.getPosition() - 1, PHPHeuristicScanner.UNBOUND);
// we inside braceless block. Now we have to move before it
if (prev == PHPHeuristicScanner.TokenRPAREN) {
// if, for, while or similar
int openParent = scanner.findOpeningPeer(scanner.getPosition(), PHPHeuristicScanner.UNBOUND,
PHPHeuristicScanner.LPAREN, PHPHeuristicScanner.RPAREN);
if (openParent == PHPHeuristicScanner.NOT_FOUND) {
return -1;
}
}
prev = scanner.previousToken(scanner.getPosition() - 1, PHPHeuristicScanner.UNBOUND);
int result = scanner.getPosition();
if (prev == PHPHeuristicScanner.TokenIF) {
prev = scanner.previousToken(start, PHPHeuristicScanner.UNBOUND);
if (prev == PHPHeuristicScanner.TokenELSE) {
result = scanner.getPosition();
}
}
return result;
} catch (BadLocationException e) {
}
return -1;
}
private boolean isMultilineContentInsideBraceless(int checkedOffset) {
try {
PHPHeuristicScanner scanner = PHPHeuristicScanner.createHeuristicScanner(document, checkedOffset - 1, true);
int start = scanner.previousToken(checkedOffset - 1, PHPHeuristicScanner.UNBOUND);
if (start == PHPHeuristicScanner.TokenLBRACE) {
if (scanner.isBracelessBlockStart(scanner.getPosition() - 1, PHPHeuristicScanner.UNBOUND)) {
return true;
}
}
} catch (BadLocationException e) {
}
return false;
}
private boolean lineShouldIndent(final String beginState, final String endState) {
return beginState == PHPPartitionTypes.PHP_DEFAULT || endState == PHPPartitionTypes.PHP_DEFAULT;
}
private boolean isInMultiLineStatement(int currLineIndex) throws BadLocationException {
return getMultiLineStatementStartOffset(currLineIndex) > -1 ? true : false;
}
/**
* @since 2.2
*/
private boolean shouldNotConsiderAsIndentationBase(final String currentState, final String forState) {
return currentState != forState && isMultilineType(currentState);
}
private boolean isMultilineAfterBraceless(int checkedOffset) {
return getMultilineInsideBraceless(checkedOffset) >= 0;
}
private boolean isMultilineType(final String state) {
return (state == PHPPartitionTypes.PHP_QUOTED_STRING) || (state == PHPPartitionTypes.PHP_MULTI_LINE_COMMENT)
|| (state == PHPPartitionTypes.PHP_DOC);
}
}