package com.aptana.editor.php.internal.ui.editor.formatting; import java.util.Arrays; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentCommand; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITypedRegion; import org.eclipse.jface.text.TextUtilities; import org.eclipse.jface.text.source.ISourceViewer; import org2.eclipse.php.internal.core.documentModel.parser.regions.PHPRegionTypes; import com.aptana.core.logging.IdeLog; import com.aptana.editor.common.contentassist.ILexemeProvider; import com.aptana.editor.php.PHPEditorPlugin; import com.aptana.editor.php.internal.contentAssist.PHPTokenType; import com.aptana.editor.php.internal.core.IPHPConstants; import com.aptana.editor.php.internal.ui.editor.PHPSourceViewerConfiguration; import com.aptana.editor.php.util.StringUtils; import com.aptana.parsing.lexer.Lexeme; public class PHPAutoIndentStrategy extends AbstractPHPAutoEditStrategy { // PHPCommentAutoIndentStrategy commentStrategy; private AbstractPHPAutoEditStrategy multiLineCommentStrategy; private AbstractPHPAutoEditStrategy switchCaseAutoEditStrategy; private AbstractPHPAutoEditStrategy alternativeSyntaxAutoEditStrategy; /** * Constructs a new PHP Auto-Indent-Strategy * * @param contentType * @param configuration * @param sourceViewer */ public PHPAutoIndentStrategy(String contentType, PHPSourceViewerConfiguration configuration, ISourceViewer sourceViewer) { super(contentType, configuration, sourceViewer); this.switchCaseAutoEditStrategy = new SwitchCaseAutoEditStrategy(contentType, configuration, sourceViewer); this.alternativeSyntaxAutoEditStrategy = new BlockEndingSyntaxAutoEditStrategy(contentType, configuration, sourceViewer); // this.commentStrategy = new PHPCommentAutoIndentStrategy(contentType, configuration, sourceViewer); this.multiLineCommentStrategy = new PhpDocAutoIndentStrategy(contentType, configuration, sourceViewer); } /** * @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument, * org.eclipse.jface.text.DocumentCommand) */ public void customizeDocumentCommand(IDocument document, DocumentCommand command) { if (!isAutoIndentEnabled()) { return; } innerCustomizeDocumentCommand(document, command); // we have to reset if for the next run this.lexemeProvider = null; } protected void innerCustomizeDocumentCommand(IDocument document, DocumentCommand command) { if (command.text == null || command.length > 0) { return; } if (command.text.equals("}")) //$NON-NLS-1$ { getLexemeProvider(document, command.offset, true); customizeCloseCurly(document, command, lexemeProvider.getFloorLexeme(command.offset)); } try { ITypedRegion region = document.getPartition(command.offset); String regionType = region.getType(); if (IPHPConstants.PHP_DOC_COMMENT.equals(regionType) || IPHPConstants.PHP_MULTI_LINE_COMMENT.equals(regionType)) { multiLineCommentStrategy.customizeDocumentCommand(document, command); return; } // if (IPHPConstants.PHP_SLASH_LINE_COMMENT.equals(regionType) // || IPHPConstants.PHP_HASH_LINE_COMMENT.equals(regionType)) // { // TODO // commentStrategy.customizeDocumentCommand(document, command); // } if (TextUtilities.endsWith(document.getLegalLineDelimiters(), command.text) != -1) { getLexemeProvider(document, command.offset, true); Lexeme<PHPTokenType> floorLexeme = lexemeProvider.getFloorLexeme(command.offset); if (floorLexeme == null || floorLexeme.getType() == null) { return; } int commandLine = document.getLineOfOffset(command.offset); if (PHPRegionTypes.WHITESPACE.equals(floorLexeme.getType().getType()) && floorLexeme.getStartingOffset() > region.getOffset()) { // Get the previous lexeme floorLexeme = lexemeProvider.getFloorLexeme(floorLexeme.getStartingOffset() - 1); int lexemeLine = document.getLineOfOffset(floorLexeme.getStartingOffset()); if (commandLine - lexemeLine > 0) { indentAfterNewLine(document, command); return; } } String lexemeText = floorLexeme.getText(); if (lexemeText.equals(":") || lexemeText.equals(";") || lexemeText.equals(")")) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ { // First, check for a case where we have a 'one-line-block' such as 'if' or 'while' without // any curly brackets of colon. if (lexemeText.equals(":")) //$NON-NLS-1$ { String indent = indentAfterOneLineBlock(floorLexeme.getStartingOffset(), document); if (indent != null) { command.text += indent; return; } } // The colon char can appear in a switch-case blocks and when using an old-style php if-else, loops // or switch-case blocks. // To decide what is the case here, we look at the first word in the current line. Lexeme<PHPTokenType> firstLexemeInLine = getFirstLexemeInLine(document, lexemeProvider, floorLexeme.getStartingOffset()); if (firstLexemeInLine == null) { return; } if (lexemeText.equals(";")) { //$NON-NLS-1$ Lexeme<PHPTokenType> previousNonWhitespaceLexeme = getPreviousNonWhitespaceLexeme(firstLexemeInLine .getStartingOffset() - 1); if (previousNonWhitespaceLexeme != null) { String indent = indentAfterOneLineBlock(previousNonWhitespaceLexeme.getStartingOffset(), document); if (indent != null) { command.text += indent; return; } } } if (lexemeText.equals(")")) //$NON-NLS-1$ { indentAfterNewLine(document, command); // command.text += configuration.getIndent(); return; } indentAfterNewLine(document, command); // This will cause a line after a 'block-type' to be indented, even when the // type has no open bracket. For now, it's disabled. If we would like to have it enabled, we should // consider de-denting the line back if the user start typing a curly-open on that line. // Check if it's one of our supported types. If so, indent. // String type = firstLexemeInLine.getType().getType(); // String indent = configuration.getIndent(); // if (BLOCK_TYPES.contains(type)) // { // command.text += indent; // } return; } if (lexemeText.equals("else") || lexemeText.equals("elseif")) //$NON-NLS-1$ //$NON-NLS-2$ { // handle 'else' and 'elseif' blocks that do not have curly-brackets indentAfterNewLine(document, command); command.text += configuration.getIndent(); } else if (lexemeText.equals("}")) //$NON-NLS-1$ { if (command.offset == floorLexeme.getStartingOffset()) { // check if the caret is right in between the brackets Lexeme<PHPTokenType> previousNonWhitespaceLexeme = getPreviousNonWhitespaceLexeme(floorLexeme .getStartingOffset() - 1); if (previousNonWhitespaceLexeme != null && "{".equals(previousNonWhitespaceLexeme.getText())) //$NON-NLS-1$ { indentAfterOpenBrace(document, command); } else { customizeCloseCurly(document, command, floorLexeme); } } else { customizeCloseCurly(document, command, floorLexeme); } } else if (!indentAfterOpenBrace(document, command)) { command.text += copyIntentationFromPreviousLine(document, command); // command.text += getIndentationAtOffset(document, floorLexeme.getStartingOffset()); } } else { // deal with cases where we would like to reduce the indentation. if (alternativeSyntaxAutoEditStrategy.isValidAutoInsertLocation(document, command)) { alternativeSyntaxAutoEditStrategy.setLexemeProvider(getLexemeProvider(document, command.offset, true)); alternativeSyntaxAutoEditStrategy.customizeDocumentCommand(document, command); } if (switchCaseAutoEditStrategy.isValidAutoInsertLocation(document, command)) { switchCaseAutoEditStrategy.setLexemeProvider(getLexemeProvider(document, command.offset, true)); switchCaseAutoEditStrategy.customizeDocumentCommand(document, command); } } } catch (BadLocationException e) { IdeLog.logError(PHPEditorPlugin.getDefault(), "Error in the PHP auto-indent strategy", e); //$NON-NLS-1$ return; } } /** * Compute the indentation in cases where a semicolon is entered and we are entering a line under a single-line * block, such as 'if' with no curly brackets, and this is the second line entered under that if.<br> * In that case, we 'dedent' the line to fit under the top-most one-line we have on top of us. * * @param offset * - The start offset to begin the lookup * @param document * @return The line indentation string; Null, if no one-line block was identified. * @throws BadLocationException */ protected String indentAfterOneLineBlock(int offset, IDocument document) throws BadLocationException { // we need to check at least two, non-empty, lines above this offset to verify // if we need to 'dedent' IRegion lineInfo = document.getLineInformationOfOffset(offset); String indent = null; if (lineInfo.getOffset() > 0) { Lexeme<PHPTokenType> firstLexemeInLine = null; int count = 10; // place it here to avoid any unexpected infinite loops.. do { if (count-- == 0) { IdeLog.logWarning( PHPEditorPlugin.getDefault(), "Stopped a possible infinite loop in the PHPAutoIndentStrategy", PHPEditorPlugin.DEBUG_SCOPE); //$NON-NLS-1$ break; } firstLexemeInLine = getFirstLexemeInLine(document, lexemeProvider, lineInfo.getOffset()); // Check if the line of lexeme ends with with a curly bracket or a colon. // If so, we have a block that span more then one line. lineInfo = document.getLineInformationOfOffset(firstLexemeInLine.getStartingOffset()); Lexeme<PHPTokenType> lastLexemeInLine = getLastLexemeInLine(document, lexemeProvider, lineInfo.getOffset()); if (lastLexemeInLine != null && firstLexemeInLine != null && (BLOCK_TYPES.contains(firstLexemeInLine.getType().getType()) || BLOCK_TYPES .contains(lastLexemeInLine.getType().getType()))) { if ("{".equals(lastLexemeInLine.getText()) || ":".equals(lastLexemeInLine.getText())) //$NON-NLS-1$//$NON-NLS-2$ { // it's a block, so return what we have if (indent == null) { return configuration.getIndent() + getIndentationAtOffset(document, firstLexemeInLine.getStartingOffset()); } return indent; } if ("else".equals(lastLexemeInLine.getText())) //$NON-NLS-1$ { return getIndentationAtOffset(document, firstLexemeInLine.getStartingOffset()); } indent = getIndentationAtOffset(document, firstLexemeInLine.getStartingOffset()); } else { break; } } while (firstLexemeInLine != null && lineInfo != null); } return indent; } /** * Handles the case where we just added a brace, and we want to return and indent to the next line * * @param d * Document * @param command * DocumentCommand * @return True if it succeeded */ protected boolean indentAfterOpenBrace(IDocument d, DocumentCommand command) { int offset = command.offset; boolean result = false; if (offset != -1 && d.getLength() != 0) { String currentLineIndent = copyIntentationFromPreviousLine(d, command); String newline = command.text; String indent = configuration.getIndent(); try { if (offset > 0) { // find the first non-whitespace char char c; do { c = d.getChar(offset - 1); if (Character.isWhitespace(c)) { offset--; } else { break; } } while (offset > 0); int offsetShift = command.offset - offset; if (c == '{') { String startIndent = newline + currentLineIndent + indent; boolean hasClosing = false; if (offset < d.getLength()) { int charOffset = offset; int charsToDelete = 0; int docLen = d.getLength(); while (charOffset < docLen) { char next = d.getChar(charOffset); if (next == '}') { hasClosing = true; break; } if (next == '\n' || next == '\r' || !Character.isWhitespace(next)) // $codepro.audit.disable // platformSpecificLineSeparator { // we could not find any closing bracket break; } charsToDelete++; charOffset++; } if (hasClosing) { // delete any whitespace chars the we have between the open brace and the close brace // before inserting the new lines d.replace(offset, charsToDelete, StringUtils.EMPTY); } } if (offset < d.getLength() && d.getChar(offset) == '}') { command.text = startIndent + newline + currentLineIndent; } else { command.text = startIndent; } command.shiftsCaret = false; command.caretOffset = command.offset + startIndent.length() - offsetShift; result = true; } } } catch (BadLocationException e) { IdeLog.logError(PHPEditorPlugin.getDefault(), "Error in the PHP auto-indent strategy", e); //$NON-NLS-1$ } } if (result) { command.offset = offset; } return result; } private void customizeCloseCurly(IDocument document, DocumentCommand command, Lexeme<PHPTokenType> curlyLexeme) { try { if (!"}".equals(command.text)) //$NON-NLS-1$ { int curlyOpenOffset = getPervPairMatchOffset("{", curlyLexeme.getStartingOffset() - 1, document); //$NON-NLS-1$ Lexeme<PHPTokenType> firstLexemeInLine = getFirstLexemeInLine(document, lexemeProvider, curlyOpenOffset); if (firstLexemeInLine != null) { command.text += getIndentationAtOffset(document, firstLexemeInLine.getStartingOffset()); } } else { Lexeme<PHPTokenType> nonWhitespaceLexeme = getFirstLexemeInLine(document, lexemeProvider, command.offset); if (nonWhitespaceLexeme == null) { // cut the spaces before the curly to 'dedent' IRegion lineRegion = document.getLineInformationOfOffset(command.offset); if (command.offset > lineRegion.getOffset()) { int length = command.offset - lineRegion.getOffset(); command.offset -= length; document.replace(lineRegion.getOffset(), length, StringUtils.EMPTY); int curlyOpenOffset = getPervPairMatchOffset("{", command.offset - 1, document); //$NON-NLS-1$ if (curlyOpenOffset < 0) { command.text = copyIntentationFromPreviousLine(document, command) + command.text; } else { Lexeme<PHPTokenType> firstLexemeInLine = getFirstLexemeInLine(document, lexemeProvider, curlyOpenOffset); if (firstLexemeInLine != null) { command.text = getIndentationAtOffset(document, firstLexemeInLine.getStartingOffset()) + command.text; } } } else { // we might need to add some indent here. check if this curly closes another curly on a line // above us int curlyOpenOffset = getPervPairMatchOffset("{", command.offset - 1, document); //$NON-NLS-1$ if (curlyOpenOffset < 0) { String indentForCurrentLine = this.copyIntentationFromPreviousLine(document, command); command.text = indentForCurrentLine + command.text; } else { Lexeme<PHPTokenType> firstLexemeInLine = getFirstLexemeInLine(document, lexemeProvider, curlyOpenOffset); if (firstLexemeInLine != null) { command.text = getIndentationAtOffset(document, firstLexemeInLine.getStartingOffset()) + command.text; } } } } } } catch (BadLocationException e) { IdeLog.logError(PHPEditorPlugin.getDefault(), "Error in the PHP auto-indent strategy", e); //$NON-NLS-1$ } } /** * Copies the indentation of the previous line. * * @param document * the document to work on * @param command * the command to deal with * @return String */ protected String copyIntentationFromPreviousLine(IDocument document, DocumentCommand command) { if (command.offset == -1 || document.getLength() == 0) { return StringUtils.EMPTY; } try { // find start of line int p = command.offset; if (p > 0) { p--; } IRegion info = document.getLineInformationOfOffset(p); int start = info.getOffset(); // find white spaces int end = findEndOfWhiteSpace(document, start, command.offset); StringBuffer buf = new StringBuffer(); if (end > start) { // append to input buf.append(document.get(start, end - start)); } return buf.toString(); } catch (BadLocationException excp) { IdeLog.logWarning( PHPEditorPlugin.getDefault(), "PHP Auto Edit Strategy - Bad location while computing an indentation (copyIntentationFromPreviousLine)", //$NON-NLS-1$ excp, PHPEditorPlugin.DEBUG_SCOPE); } return StringUtils.EMPTY; } /** * canOverwriteBracket * * @param bracket * @param offset * @param document * @param ll * @return boolean */ public boolean canOverwriteBracket(char bracket, int offset, IDocument document, ILexemeProvider<PHPTokenType> ll) { if (offset < document.getLength()) { char[] autoOverwriteChars = getAutoOverwriteCharacters(); Arrays.sort(autoOverwriteChars); if (Arrays.binarySearch(autoOverwriteChars, bracket) < 0) { return false; } try { char sibling = document.getChar(offset); return sibling == bracket; } catch (BadLocationException ex) { return false; } } return false; } }