/******************************************************************************* * Copyright (c) 2009, 2016, 2017 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 org.apache.commons.lang3.StringUtils; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IRegion; import org.eclipse.php.internal.core.Logger; 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.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; import org.eclipse.wst.sse.core.internal.text.rules.SimpleStructuredRegion; public class PHPIndentationFormatter { private final IIndentationStrategy defaultIndentationStrategy; private final IIndentationStrategy curlyCloseIndentationStrategy; private final IIndentationStrategy caseDefaultIndentationStrategy; private final IIndentationStrategy commentIndentationStrategy; private final IIndentationStrategy phpCloseTagIndentationStrategy; private final int length; private final int start; private static final byte CHAR_TAB = '\t'; private static final byte CHAR_SPACE = ' '; private final StringBuilder resultBuffer = new StringBuilder(); private final StringBuilder lastEmptyLineIndentationBuffer = new StringBuilder(); private int lastEmptyLineNumber; public PHPIndentationFormatter(int start, int length, IndentationObject indentationObject) { this.start = start; this.length = length; this.defaultIndentationStrategy = new DefaultIndentationStrategy(indentationObject); this.curlyCloseIndentationStrategy = new CurlyCloseIndentationStrategy(); this.caseDefaultIndentationStrategy = new CaseDefaultIndentationStrategy(indentationObject); this.commentIndentationStrategy = new CommentIndentationStrategy(indentationObject); this.phpCloseTagIndentationStrategy = new PHPCloseTagIndentationStrategy(indentationObject); } // XXX: also give the possibility to modify this.start, this.length and // indentationObject? protected void reset() { resultBuffer.setLength(0); lastEmptyLineIndentationBuffer.setLength(0); lastEmptyLineNumber = -1; } public void format(IStructuredDocumentRegion sdRegion) { assert sdRegion != null; reset(); // resolve formatter range int regionStart = sdRegion.getStartOffset(); int regionEnd = sdRegion.getEndOffset(); int formatRequestStart = getStart(); int formatRequestEnd = formatRequestStart + getLength(); int startFormat = Math.max(formatRequestStart, regionStart); int endFormat = Math.min(formatRequestEnd, regionEnd); // calculate lines IStructuredDocument document = sdRegion.getParentDocument(); int lineIndex = document.getLineOfOffset(startFormat); int endLineIndex = document.getLineOfOffset(endFormat); // TODO get token of each line then insert line separator after { and // after } if there is no line separator // format each line for (; lineIndex <= endLineIndex; lineIndex++) { formatLine(document, lineIndex); } reset(); } private void doEmptyLineIndentation(IStructuredDocument document, StringBuilder result, int lineNumber, int forOffset, int forLength) throws BadLocationException { if (lastEmptyLineNumber >= 0 && lastEmptyLineNumber == lineNumber - 1) { // re-use previous line indentation if previous line was also an // empty line (to avoid unnecessary calls to placeMatchingBlanks()) result.append(lastEmptyLineIndentationBuffer); } else { getDefaultIndentationStrategy().placeMatchingBlanks(document, result, lineNumber, forOffset); lastEmptyLineIndentationBuffer.setLength(0); lastEmptyLineIndentationBuffer.append(result); } lastEmptyLineNumber = lineNumber; if (!(forLength == 0 && result.length() == 0)) { document.replace(forOffset, forLength, result.toString()); } } /** * formats a PHP line according to the strategies and formatting conventions * * @param document * @param lineNumber */ private void formatLine(IStructuredDocument document, int lineNumber) { resultBuffer.setLength(0); try { // get original line information final IRegion originalLineInfo = document.getLineInformation(lineNumber); final int originalLineStart = originalLineInfo.getOffset(); int originalLineLength = originalLineInfo.getLength(); // get formatted line information final String lineText = document.get(originalLineStart, originalLineLength); final IRegion formattedLineInformation = getFormattedLineInformation(originalLineInfo, lineText); if (!shouldReformat(document, formattedLineInformation)) { return; } // fast resolving of empty line if (originalLineLength == 0) { doEmptyLineIndentation(document, resultBuffer, lineNumber, originalLineStart, originalLineLength); return; } // remove ending spaces. final int formattedLineStart = formattedLineInformation.getOffset(); final int formattedTextEnd = formattedLineStart + formattedLineInformation.getLength(); if (formattedTextEnd != originalLineStart + originalLineLength) { // resolve blank line if (formattedLineStart == formattedTextEnd) { doEmptyLineIndentation(document, resultBuffer, lineNumber, originalLineStart, originalLineLength); return; } document.replace(formattedTextEnd, originalLineStart + originalLineLength - formattedTextEnd, ""); //$NON-NLS-1$ originalLineLength = formattedTextEnd - originalLineStart; } // get regions final int endingWhiteSpaces = formattedLineStart - originalLineStart; final IIndentationStrategy insertionStrategy; final IStructuredDocumentRegion sdRegion = document.getRegionAtCharacterOffset(formattedLineStart); // int scriptRegionPos = sdRegion.getStartOffset(); ITextRegion firstTokenInLine = sdRegion.getRegionAtCharacterOffset(formattedLineStart); ITextRegion lastTokenInLine = null; int regionStart = firstTokenInLine != null ? sdRegion.getStartOffset(firstTokenInLine) : 0; if (firstTokenInLine instanceof ITextRegionContainer) { // scriptRegionPos = regionStart; ITextRegionContainer container = (ITextRegionContainer) firstTokenInLine; firstTokenInLine = container.getRegionAtCharacterOffset(formattedLineStart); regionStart += firstTokenInLine.getStart(); } if (firstTokenInLine instanceof IPHPScriptRegion) { IPHPScriptRegion scriptRegion = (IPHPScriptRegion) firstTokenInLine; assert regionStart + scriptRegion.getEnd() > formattedLineStart; if (scriptRegion.isPHPQuotesState(formattedLineStart - regionStart) && (formattedLineStart - regionStart == 0 || scriptRegion.isPHPQuotesState(formattedLineStart - regionStart - 1))) { // do never indent the content of php strings return; } // scriptRegionPos = regionStart; firstTokenInLine = scriptRegion.getPHPToken(formattedLineStart - regionStart); if (regionStart + firstTokenInLine.getStart() < originalLineStart && firstTokenInLine.getType() == PHPRegionTypes.WHITESPACE) { firstTokenInLine = scriptRegion.getPHPToken(firstTokenInLine.getEnd()); } if (formattedTextEnd <= regionStart + scriptRegion.getEnd()) { lastTokenInLine = scriptRegion.getPHPToken(formattedTextEnd - regionStart - 1); if (regionStart + lastTokenInLine.getEnd() > originalLineStart + originalLineLength && lastTokenInLine.getType() == PHPRegionTypes.WHITESPACE && lastTokenInLine.getStart() > 0) { lastTokenInLine = scriptRegion.getPHPToken(lastTokenInLine.getStart() - 1); } } } // if the next char is not from this line if (firstTokenInLine == null) { doEmptyLineIndentation(document, resultBuffer, lineNumber, originalLineStart, 0); return; } String firstTokenType = firstTokenInLine.getType(); if (firstTokenType == PHPRegionTypes.PHP_CASE || firstTokenType == PHPRegionTypes.PHP_DEFAULT) { insertionStrategy = caseDefaultIndentationStrategy; } else if (isInsideOfPHPCommentRegion(firstTokenType)) { insertionStrategy = commentIndentationStrategy; } else if (firstTokenType == PHPRegionTypes.PHP_CLOSETAG) { insertionStrategy = phpCloseTagIndentationStrategy; } else { insertionStrategy = getIndentationStrategy(lineText.charAt(endingWhiteSpaces)); } insertionStrategy.placeMatchingBlanks(document, resultBuffer, lineNumber, originalLineStart); // replace the starting spaces final String newIndentation = resultBuffer.toString(); final String oldIndentation = lineText.substring(0, endingWhiteSpaces); if (!StringUtils.equals(oldIndentation, newIndentation)) { document.replace(originalLineStart, endingWhiteSpaces, newIndentation); } } catch (BadLocationException e) { Logger.logException(e); } } /** * @return whether we are inside a php comment */ private boolean isInsideOfPHPCommentRegion(String tokenType) { return PHPPartitionTypes.isPHPMultiLineCommentRegion(tokenType) || PHPPartitionTypes.isPHPMultiLineCommentEndRegion(tokenType) || PHPPartitionTypes.isPHPDocRegion(tokenType) || PHPPartitionTypes.isPHPDocEndRegion(tokenType); } /** * @return the formatted line (without whitespaces) information */ private IRegion getFormattedLineInformation(IRegion lineInfo, String lineText) { // start checking from left and right to the center int leftNonWhitespaceChar = 0; int rightNonWhitespaceChar = lineText.length() - 1; final char[] chars = lineText.toCharArray(); for (; leftNonWhitespaceChar <= rightNonWhitespaceChar; leftNonWhitespaceChar++) { if (chars[leftNonWhitespaceChar] != CHAR_SPACE && chars[leftNonWhitespaceChar] != CHAR_TAB) { break; } } for (; leftNonWhitespaceChar <= rightNonWhitespaceChar; rightNonWhitespaceChar--) { if (chars[rightNonWhitespaceChar] != CHAR_SPACE && chars[rightNonWhitespaceChar] != CHAR_TAB) { break; } } // if line is empty then the indexes were switched if (leftNonWhitespaceChar > rightNonWhitespaceChar) return new SimpleStructuredRegion(lineInfo.getOffset(), 0); // if there are no changes - return the original line information, else // build a fixed region return leftNonWhitespaceChar == 0 && rightNonWhitespaceChar == lineText.length() - 1 ? lineInfo : new SimpleStructuredRegion(lineInfo.getOffset() + leftNonWhitespaceChar, rightNonWhitespaceChar - leftNonWhitespaceChar + 1); } private boolean shouldReformat(IStructuredDocument document, IRegion lineInfo) { final String checkedLineBeginState = FormatterUtils.getPartitionType(document, lineInfo.getOffset()); return ((checkedLineBeginState == PHPPartitionTypes.PHP_DEFAULT) || (checkedLineBeginState == PHPPartitionTypes.PHP_MULTI_LINE_COMMENT) || (checkedLineBeginState == PHPPartitionTypes.PHP_SINGLE_LINE_COMMENT) || (checkedLineBeginState == PHPPartitionTypes.PHP_DOC) || (checkedLineBeginState == PHPPartitionTypes.PHP_QUOTED_STRING)); } protected final int getStart() { return start; } protected final int getLength() { return length; } protected IIndentationStrategy getIndentationStrategy(char c) { if (c == '}') { return curlyCloseIndentationStrategy; } return getDefaultIndentationStrategy(); } private IIndentationStrategy getDefaultIndentationStrategy() { return defaultIndentationStrategy; } }