/******************************************************************************* * Copyright (c) 2009, 2015 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 * Dawid PakuĊ‚a [459462] *******************************************************************************/ package org.eclipse.php.internal.core.format; import org.apache.commons.lang3.StringUtils; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.Region; import org.eclipse.php.internal.core.ast.util.Util; 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.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 DefaultIndentationStrategy implements IIndentationStrategy { private static final String BLANK = ""; //$NON-NLS-1$ private static boolean pairArrayParen; private static int pairArrayOffset; private IndentationObject indentationObject; public DefaultIndentationStrategy() { } /** * * @param indentationObject * basic indentation preferences, can be null */ public DefaultIndentationStrategy(IndentationObject indentationObject) { this.indentationObject = indentationObject; } public void setIndentationObject(IndentationObject indentationObject) { this.indentationObject = indentationObject; } // go backward and look for any region except comment region or white space // region // in the given line private static ITextRegion getLastTokenRegion(final IStructuredDocument document, final IRegion line, final int forOffset) throws BadLocationException { int offset = forOffset; int lineStartOffset = line.getOffset(); IStructuredDocumentRegion sdRegion = document.getRegionAtCharacterOffset(offset); if (sdRegion == null) { return null; } ITextRegion tRegion = sdRegion.getRegionAtCharacterOffset(offset); if (tRegion == null && offset == document.getLength()) { offset -= 1; tRegion = sdRegion.getRegionAtCharacterOffset(offset); } int regionStart = sdRegion.getStartOffset(tRegion); // in case of container we have to extract the PhpScriptRegion if (tRegion instanceof ITextRegionContainer) { ITextRegionContainer container = (ITextRegionContainer) tRegion; tRegion = container.getRegionAtCharacterOffset(offset); regionStart += tRegion.getStart(); } if (tRegion instanceof IPHPScriptRegion) { IPHPScriptRegion scriptRegion = (IPHPScriptRegion) tRegion; tRegion = scriptRegion.getPHPToken(offset - regionStart); // go backward over the region to find a region (not comment nor // whitespace) // in the same line do { String token = tRegion.getType(); if (regionStart + tRegion.getStart() >= forOffset) { // making sure the region found is not after the caret // (https://bugs.eclipse.org/bugs/show_bug.cgi?id=222019 - // caret before '{') } else if (!PHPPartitionTypes.isPHPCommentState(token) && token != PHPRegionTypes.WHITESPACE) { // not comment nor white space return tRegion; } if (tRegion.getStart() >= 1) { tRegion = scriptRegion.getPHPToken(tRegion.getStart() - 1); } else { tRegion = null; } } while (tRegion != null && tRegion.getStart() + regionStart >= lineStartOffset); } return null; } public void placeMatchingBlanks(final IStructuredDocument document, final StringBuilder result, final int lineNumber, final int forOffset) throws BadLocationException { placeMatchingBlanksForStructuredDocument(document, result, lineNumber, forOffset, getCommandText()); } public void placeMatchingBlanksForStructuredDocument(final IStructuredDocument document, final StringBuilder result, final int lineNumber, final int forOffset) throws BadLocationException { placeMatchingBlanksForStructuredDocument(document, result, lineNumber, forOffset, BLANK); } private void placeMatchingBlanksForStructuredDocument(final IStructuredDocument document, final StringBuilder result, final int lineNumber, final int forOffset, String commandText) throws BadLocationException { if (forOffset == 0) { return; } if (indentationObject == null) { indentationObject = new IndentationObject(document); } boolean enterKeyPressed = document.getLineDelimiter().equals(result.toString()); int lineOfOffset = document.getLineOfOffset(forOffset); IRegion lineInformationOfOffset = document.getLineInformation(lineOfOffset); int lastNonEmptyLineIndex; final int indentationBaseLineIndex; final int newForOffset; // code for not formatting comments if (enterKeyPressed && document.get(lineInformationOfOffset.getOffset(), lineInformationOfOffset.getLength()) .trim().startsWith("//")) { //$NON-NLS-1$ lastNonEmptyLineIndex = lineOfOffset; indentationBaseLineIndex = lineOfOffset; int i = lineInformationOfOffset.getOffset(); for (; i < lineInformationOfOffset.getOffset() + lineInformationOfOffset.getLength() && document.getChar(i) != '/'; i++) ; newForOffset = (i < forOffset) ? i : forOffset; } // end else { newForOffset = forOffset; IndentationBaseDetector indentationDetector = new IndentationBaseDetector(document, lineNumber, newForOffset); lastNonEmptyLineIndex = indentationDetector.getIndentationBaseLine(false); indentationBaseLineIndex = indentationDetector.getIndentationBaseLine(true); } final IRegion lastNonEmptyLine = document.getLineInformation(lastNonEmptyLineIndex); final IRegion indentationBaseLine = document.getLineInformation(indentationBaseLineIndex); final String blanks = FormatterUtils.getLineBlanks(document, indentationBaseLine); result.append(blanks); final int lastLineEndOffset = lastNonEmptyLine.getOffset() + lastNonEmptyLine.getLength(); int offset; int line; if (newForOffset < lastLineEndOffset) { offset = newForOffset; line = lineNumber; } else { offset = lastLineEndOffset; line = lastNonEmptyLineIndex; } if (shouldIndent(document, offset, line)) { indent(document, result, indentationObject.getIndentationChar(), indentationObject.getIndentationSize()); } else { boolean intended = indentMultiLineCase(document, lineNumber, newForOffset, enterKeyPressed, result, blanks, commandText, indentationObject); if (!intended) { lastNonEmptyLineIndex = lineNumber; if (!enterKeyPressed && lastNonEmptyLineIndex > 0) { lastNonEmptyLineIndex--; } while (lastNonEmptyLineIndex >= 0) { IRegion lineInfo = document.getLineInformation(lastNonEmptyLineIndex); String content = document.get(lineInfo.getOffset(), lineInfo.getLength()); if (StringUtils.isNotBlank(content)) { break; } lastNonEmptyLineIndex--; } if (!isEndOfStatement(document, offset, lastNonEmptyLineIndex)) { if (indentationBaseLineIndex == lastNonEmptyLineIndex) { // this only deal with "$a = 'aaa'.|","|" is the // cursor // position when we press enter key placeStringIndentation(document, lastNonEmptyLineIndex, result, indentationObject); } // if (enterKeyPressed) { // this line is one of multi line statement // in multi line statement,when user press enter // key, // we use the same indentation of the last non-empty // line. boolean shouldNotChangeIndent = false; if (newForOffset != document.getLength()) { final IRegion lineInfo = document.getLineInformation(lineNumber); int nonEmptyOffset = newForOffset; if (!enterKeyPressed) { if (nonEmptyOffset == lineInfo.getOffset()) { nonEmptyOffset = IndentationUtils.moveLineStartToNonBlankChar(document, nonEmptyOffset, lineNumber, false); } } char lineStartChar = document.getChar(nonEmptyOffset); if (lineStartChar == PHPHeuristicScanner.RBRACE // || lineStartChar == // PHPHeuristicScanner.RBRACKET || lineStartChar == PHPHeuristicScanner.RPAREN) { PHPHeuristicScanner scanner = PHPHeuristicScanner.createHeuristicScanner(document, nonEmptyOffset, true); if (lineStartChar == PHPHeuristicScanner.RBRACE) { int peer = scanner.findOpeningPeer(nonEmptyOffset - 1, PHPHeuristicScanner.UNBOUND, PHPHeuristicScanner.LBRACE, PHPHeuristicScanner.RBRACE); if (peer != PHPHeuristicScanner.NOT_FOUND) { shouldNotChangeIndent = true; } } else if (lineStartChar == PHPHeuristicScanner.RBRACKET) { int peer = scanner.findOpeningPeer(nonEmptyOffset - 1, PHPHeuristicScanner.UNBOUND, PHPHeuristicScanner.LBRACKET, PHPHeuristicScanner.RBRACKET); if (peer != PHPHeuristicScanner.NOT_FOUND) { shouldNotChangeIndent = true; } } else if (lineStartChar == PHPHeuristicScanner.RPAREN) { int peer = scanner.findOpeningPeer(nonEmptyOffset - 1, PHPHeuristicScanner.UNBOUND, PHPHeuristicScanner.LPAREN, PHPHeuristicScanner.RPAREN); if (peer != PHPHeuristicScanner.NOT_FOUND) { shouldNotChangeIndent = true; } } } } if (!shouldNotChangeIndent) { result.setLength(result.length() - blanks.length()); IRegion lineInfo = document.getLineInformation(lastNonEmptyLineIndex); result.append(FormatterUtils.getLineBlanks(document, lineInfo)); } // } } else {// current is a new statement,check if we should indent // it based on indentationBaseLine if (result.length() == blanks.length()) { final int baseLineEndOffset = indentationBaseLine.getOffset() + indentationBaseLine.getLength(); offset = baseLineEndOffset; line = indentationBaseLineIndex; // check if after braceless if (shouldIndent(document, offset, line)) { indent(document, result, indentationObject.getIndentationChar(), indentationObject.getIndentationSize()); } } } } } } private static void indent(final IStructuredDocument document, final StringBuilder result, int indentationChar, int indentationSize) { for (int i = 0; i < indentationSize; i++) { result.append((char) indentationChar); } } private static boolean indentMultiLineCase(IStructuredDocument document, int lineNumber, int offset, boolean enterKeyPressed, StringBuilder result, String blanks, String commandText, IndentationObject indentationObject) { // LineState lineState = new LineState(); try { IRegion region = document.getLineInformationOfOffset(offset); String content = document.get(offset, region.getOffset() + region.getLength() - offset); PHPHeuristicScanner scanner = PHPHeuristicScanner.createHeuristicScanner(document, offset, true); if (enterKeyPressed && content.trim().startsWith("//")) { //$NON-NLS-1$ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=457701 return true; } else if (IndentationUtils.inBracelessBlock(scanner, document, offset)) { // lineState.inBracelessBlock = true; if (!"{".equals(commandText)) { //$NON-NLS-1$ indent(document, result, indentationObject.getIndentationChar(), indentationObject.getIndentationSize()); } return true; } else if (content.trim().startsWith(BLANK + PHPHeuristicScanner.LBRACE)) { // lineState.inBracelessBlock = true; int token = scanner.previousToken(offset - 1, PHPHeuristicScanner.UNBOUND); if (token == PHPHeuristicScanner.TokenRPAREN) { int peer = scanner.findOpeningPeer(scanner.getPosition(), PHPHeuristicScanner.UNBOUND, PHPHeuristicScanner.LPAREN, PHPHeuristicScanner.RPAREN); if (peer != PHPHeuristicScanner.NOT_FOUND) { String newblanks = FormatterUtils.getLineBlanks(document, document.getLineInformationOfOffset(peer)); StringBuilder newBuffer = new StringBuilder(newblanks); // IRegion region = document // .getLineInformationOfOffset(offset); result.setLength(result.length() - blanks.length()); result.append(newBuffer.toString()); return true; } } } else if (inMultiLine(scanner, document, lineNumber, offset)) { // lineState.inBracelessBlock = true; int parenPeer = scanner.findOpeningPeer(offset - 1, PHPHeuristicScanner.UNBOUND, PHPHeuristicScanner.LPAREN, PHPHeuristicScanner.RPAREN); int bound = parenPeer != -1 ? parenPeer : PHPHeuristicScanner.UNBOUND; int bracketPeer = scanner.findOpeningPeer(offset - 1, bound, PHPHeuristicScanner.LBRACKET, PHPHeuristicScanner.RBRACKET); int peer = Math.max(parenPeer, bracketPeer); if (peer != PHPHeuristicScanner.NOT_FOUND) { // search for assignment (i.e. "=>") int position = peer - 1; int token = scanner.previousToken(position, PHPHeuristicScanner.UNBOUND); // scan tokens backwards until reaching a PHP token while (token > 100 || token == PHPHeuristicScanner.TokenOTHER) { position--; token = scanner.previousToken(position, PHPHeuristicScanner.UNBOUND); } position--; boolean isAssignment = scanner.previousToken(position, PHPHeuristicScanner.UNBOUND) == PHPHeuristicScanner.TokenGREATERTHAN && scanner.previousToken(position - 1, PHPHeuristicScanner.UNBOUND) == PHPHeuristicScanner.TokenEQUAL; token = scanner.previousToken(peer - 1, PHPHeuristicScanner.UNBOUND); boolean isArray = token == Symbols.TokenARRAY || peer == bracketPeer; String newblanks = FormatterUtils.getLineBlanks(document, document.getLineInformationOfOffset(peer)); StringBuilder newBuffer = new StringBuilder(newblanks); pairArrayParen = false; if (isArray) { String trimed = document.get(offset, region.getOffset() + region.getLength() - offset).trim(); if (enterKeyPressed || !(trimed.startsWith(BLANK + PHPHeuristicScanner.RPAREN) || trimed.startsWith(BLANK + PHPHeuristicScanner.RBRACKET))) { int arrayBracket = scanner.nextToken(offset, region.getOffset() + region.getLength()); if (enterKeyPressed && (arrayBracket == PHPHeuristicScanner.TokenRPAREN || arrayBracket == PHPHeuristicScanner.TokenRBRACKET)) { int prev = scanner.previousToken(offset - 1, PHPHeuristicScanner.UNBOUND); if (isAssignment && ((arrayBracket == PHPHeuristicScanner.TokenRPAREN && prev != PHPHeuristicScanner.TokenLPAREN) || (arrayBracket == PHPHeuristicScanner.TokenRBRACKET && prev != PHPHeuristicScanner.TokenLBRACKET))) { // no additional indentation } else { indent(document, newBuffer, indentationObject.getIndentationArrayInitSize(), indentationObject.getIndentationChar(), indentationObject.getIndentationSize()); pairArrayParen = true; } } else { indent(document, newBuffer, indentationObject.getIndentationArrayInitSize(), indentationObject.getIndentationChar(), indentationObject.getIndentationSize()); } } } else { indent(document, newBuffer, indentationObject.getIndentationWrappedLineSize(), indentationObject.getIndentationChar(), indentationObject.getIndentationSize()); } result.setLength(result.length() - blanks.length()); result.append(newBuffer.toString()); if (pairArrayParen) { pairArrayOffset = offset + result.length(); result.append(Util.getLineSeparator(null, null)); result.append(blanks); } return true; } } else { int baseLine = inMultiLineString(document, offset, lineNumber, enterKeyPressed); if (baseLine >= 0) { String newblanks = FormatterUtils.getLineBlanks(document, document.getLineInformation(baseLine)); StringBuilder newBuffer = new StringBuilder(newblanks); indent(document, newBuffer, indentationObject.getIndentationWrappedLineSize(), indentationObject.getIndentationChar(), indentationObject.getIndentationSize()); result.setLength(result.length() - blanks.length()); result.append(newBuffer.toString()); return true; } } } catch (final BadLocationException e) { } return false; } private static void indent(IStructuredDocument document, StringBuilder indent, int times, int indentationChar, int indentationSize) { for (int i = 0; i < times; i++) { indent(document, indent, indentationChar, indentationSize); } } private static boolean inMultiLine(PHPHeuristicScanner scanner, IStructuredDocument document, int lineNumber, int offset) { int lineStartOffset = offset; try { IRegion region = document.getLineInformation(lineNumber); int start = lineStartOffset; int end = region.getOffset() + region.getLength() - lineStartOffset; for (int i = start; i < end; i++) { if (Character.isWhitespace(document.getChar(i))) { } else { // move line start to first non blank char // and do + 1 to adjust offset of // PHPTextSequenceUtilities.getStatement(...) lineStartOffset += i + 1; break; } } } catch (BadLocationException e) { } Region textSequenceRegion = PHPTextSequenceUtilities.getStatementRegion(lineStartOffset, document.getRegionAtCharacterOffset(lineStartOffset), true); if (textSequenceRegion.getLength() == 0) { return false; } String regionType = FormatterUtils.getRegionType(document, textSequenceRegion.getOffset()); if (IndentationUtils.isRegionTypeAllowedMultiline(regionType)) { int statementStart = textSequenceRegion.getOffset(); // we only search for opening pear in textSequence int bound = statementStart; int parenPeer = scanner.findOpeningPeer(offset - 1, bound, PHPHeuristicScanner.LPAREN, PHPHeuristicScanner.RPAREN); bound = parenPeer != -1 ? Math.max(parenPeer, bound) : bound; int bracketPeer = scanner.findOpeningPeer(offset - 1, bound, PHPHeuristicScanner.LBRACKET, PHPHeuristicScanner.RBRACKET); int peer = Math.max(parenPeer, bracketPeer); if (peer == PHPHeuristicScanner.NOT_FOUND) { return false; } if (statementStart < peer) { return true; } } return false; } private static int inMultiLineString(IStructuredDocument document, int offset, int lineNumber, boolean enterKeyPressed) { try { IRegion lineInfo = document.getLineInformation(lineNumber); ITextRegion token = getLastTokenRegion(document, lineInfo, offset); if (token == null) return -1; String tokenType = token.getType(); if (tokenType == PHPRegionTypes.PHP_CONSTANT_ENCAPSED_STRING) { int startLine = document.getLineOfOffset(token.getStart()); if (enterKeyPressed && startLine <= lineNumber || !enterKeyPressed && startLine < lineNumber) { return startLine; } } } catch (BadLocationException e) { } return -1; } private static boolean isEndOfStatement(IStructuredDocument document, int offset, int lineNumber) { try { IRegion lineInfo = document.getLineInformation(lineNumber); ITextRegion token = getLastTokenRegion(document, lineInfo, lineInfo.getOffset() + lineInfo.getLength()); if (token == null)// comment return true; if (token.getType() == PHPRegionTypes.PHP_SEMICOLON || token.getType() == PHPRegionTypes.PHP_CURLY_CLOSE) { return true; } else if (token.getType() == PHPRegionTypes.PHP_HEREDOC_CLOSE_TAG || token.getType() == PHPRegionTypes.PHP_NOWDOC_CLOSE_TAG) { return true; } } catch (final BadLocationException e) { } return false; } private static void placeStringIndentation(final IStructuredDocument document, int lineNumber, StringBuilder result, IndentationObject indentationObject) { try { IRegion lineInfo = document.getLineInformation(lineNumber); int offset = lineInfo.getOffset() + lineInfo.getLength(); final IStructuredDocumentRegion sdRegion = document.getRegionAtCharacterOffset(offset); ITextRegion token = getLastTokenRegion(document, lineInfo, offset); if (token == null) return; String tokenType = token.getType(); if (tokenType == PHPRegionTypes.PHP_CURLY_OPEN) return; ITextRegion scriptRegion = sdRegion.getRegionAtCharacterOffset(offset); if (scriptRegion == null && offset == document.getLength()) { offset -= 1; scriptRegion = sdRegion.getRegionAtCharacterOffset(offset); } int regionStart = sdRegion.getStartOffset(scriptRegion); // in case of container we have to extract the PhpScriptRegion if (scriptRegion instanceof ITextRegionContainer) { ITextRegionContainer container = (ITextRegionContainer) scriptRegion; scriptRegion = container.getRegionAtCharacterOffset(offset); regionStart += scriptRegion.getStart(); } if (scriptRegion instanceof IPHPScriptRegion) { if (tokenType == PHPRegionTypes.PHP_TOKEN && document.getChar(regionStart + token.getStart()) == '.') { token = ((IPHPScriptRegion) scriptRegion).getPHPToken(token.getStart() - 1); if (token.getType() == PHPRegionTypes.PHP_CONSTANT_ENCAPSED_STRING) { boolean isToken = true; int currentOffset = regionStart + token.getStart() - 1; while (currentOffset >= lineInfo.getOffset()) { token = ((IPHPScriptRegion) scriptRegion).getPHPToken(token.getStart() - 1); tokenType = token.getType(); if (isToken && (tokenType == PHPRegionTypes.PHP_TOKEN && document.getChar(regionStart + token.getStart()) == '.') || !isToken && tokenType == PHPRegionTypes.PHP_CONSTANT_ENCAPSED_STRING) { currentOffset = regionStart + token.getStart() - 1; } else { break; } } indent(document, result, indentationObject.getIndentationWrappedLineSize(), indentationObject.getIndentationChar(), indentationObject.getIndentationSize()); } } } } catch (final BadLocationException e) { } } private static boolean shouldIndent(final IStructuredDocument document, int offset, final int lineNumber) { try { final IRegion lineInfo = document.getLineInformation(lineNumber); final IStructuredDocumentRegion sdRegion = document.getRegionAtCharacterOffset(offset); ITextRegion token = getLastTokenRegion(document, lineInfo, offset); if (token == null) return false; String tokenType = token.getType(); if (tokenType == PHPRegionTypes.PHP_CURLY_OPEN) return true; ITextRegion scriptRegion = sdRegion.getRegionAtCharacterOffset(offset); if (scriptRegion == null && offset == document.getLength()) { offset -= 1; scriptRegion = sdRegion.getRegionAtCharacterOffset(offset); } int regionStart = sdRegion.getStartOffset(scriptRegion); // in case of container we have to extract the PhpScriptRegion if (scriptRegion instanceof ITextRegionContainer) { ITextRegionContainer container = (ITextRegionContainer) scriptRegion; scriptRegion = container.getRegionAtCharacterOffset(offset); regionStart += scriptRegion.getStart(); } if (scriptRegion instanceof IPHPScriptRegion) { if (tokenType == PHPRegionTypes.PHP_TOKEN && document.getChar(regionStart + token.getStart()) == ':') { // checking if the line starts with "case" or "default" int currentOffset = regionStart + token.getStart() - 1; while (currentOffset >= lineInfo.getOffset()) { token = ((IPHPScriptRegion) scriptRegion).getPHPToken(token.getStart() - 1); tokenType = token.getType(); if (tokenType == PHPRegionTypes.PHP_CASE || tokenType == PHPRegionTypes.PHP_DEFAULT) return true; currentOffset = regionStart + token.getStart() - 1; } } } } catch (final BadLocationException e) { } return false; } protected String getCommandText() { return BLANK; } public static int getPairArrayOffset() { // TODO Auto-generated method stub if (pairArrayParen) { return pairArrayOffset; } return -1; } public static boolean getPairArrayParen() { // TODO Auto-generated method stub return pairArrayParen; } public static void unsetPairArrayParen() { // TODO Auto-generated method stub pairArrayParen = false; } }