package com.aptana.editor.php.internal.ui.editor.formatting; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.Stack; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentCommand; import org.eclipse.jface.text.IAutoEditStrategy; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; 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.common.contentassist.LexemeProvider; import com.aptana.editor.common.preferences.IPreferenceConstants; import com.aptana.editor.php.PHPEditorPlugin; import com.aptana.editor.php.epl.PHPEplPlugin; import com.aptana.editor.php.internal.contentAssist.PHPTokenType; import com.aptana.editor.php.internal.contentAssist.ParsingUtils; import com.aptana.editor.php.internal.ui.editor.PHPSourceViewerConfiguration; import com.aptana.editor.php.util.StringUtils; import com.aptana.parsing.lexer.Lexeme; /** * Base class for PHP auto-indent strategies. * * @author Shalom Gibly <sgibly@aptana.com> */ public class AbstractPHPAutoEditStrategy implements IAutoEditStrategy { /** * A set of PHP alternate end types, such as endif, endfor and such. */ protected static Set<String> ALTERNATIVE_END_STYLES = new HashSet<String>(Arrays.asList( PHPRegionTypes.PHP_ENDDECLARE, PHPRegionTypes.PHP_ENDFOR, PHPRegionTypes.PHP_ENDFOREACH, PHPRegionTypes.PHP_ENDIF, PHPRegionTypes.PHP_ENDSWITCH, PHPRegionTypes.PHP_ENDWHILE)); /** * A set of possible PHP alternate start types, such as if, for and such. */ protected static Set<String> ALTERNATIVE_START_STYLES = new HashSet<String>(Arrays.asList( PHPRegionTypes.PHP_DECLARE, PHPRegionTypes.PHP_FOR, PHPRegionTypes.PHP_FOREACH, PHPRegionTypes.PHP_IF, PHPRegionTypes.PHP_SWITCH, PHPRegionTypes.PHP_WHILE)); /** * A set of PHP block types, such as for, while and such. */ protected static Set<String> BLOCK_TYPES = new HashSet<String>(Arrays.asList(PHPRegionTypes.PHP_IF, PHPRegionTypes.PHP_FOR, PHPRegionTypes.PHP_FOREACH, PHPRegionTypes.PHP_ELSEIF, PHPRegionTypes.PHP_ELSE, PHPRegionTypes.PHP_SWITCH, PHPRegionTypes.PHP_WHILE, PHPRegionTypes.PHP_CASE, PHPRegionTypes.PHP_DEFAULT)); /** * When we hit those while searching for a pair match, we can tell for sure we are out of the search scope. */ protected static Set<String> TERMINATORS = new HashSet<String>(Arrays.asList(PHPRegionTypes.PHP_FUNCTION, PHPRegionTypes.PHP_CLASS, PHPRegionTypes.PHP_PUBLIC, PHPRegionTypes.PHP_PROTECTED, PHPRegionTypes.PHP_PRIVATE)); /** * Holds the open pair match for the closing php element */ protected static Map<String, String> PAIR_MATCH = new HashMap<String, String>(); static { // Load the pair matches PAIR_MATCH.put(PHPRegionTypes.PHP_ENDIF, PHPRegionTypes.PHP_IF); PAIR_MATCH.put(PHPRegionTypes.PHP_ENDDECLARE, PHPRegionTypes.PHP_DECLARE); PAIR_MATCH.put(PHPRegionTypes.PHP_ENDFOR, PHPRegionTypes.PHP_FOR); PAIR_MATCH.put(PHPRegionTypes.PHP_ENDFOREACH, PHPRegionTypes.PHP_FOREACH); PAIR_MATCH.put(PHPRegionTypes.PHP_ENDSWITCH, PHPRegionTypes.PHP_SWITCH); PAIR_MATCH.put(PHPRegionTypes.PHP_ENDWHILE, PHPRegionTypes.PHP_WHILE); } /** * To avoid a performance issue of lexing very big files on every type, we limit the lexer to a maximum of 1000 * chars from the current offset of the edit.<br> * This should cover most cases, even when we have a comment region that we scan trough. */ protected static final int MAX_CHARS_TO_LEX_BACK = 1000; /** * spaces */ protected String spaces = " "; //$NON-NLS-1$ protected ILexemeProvider<PHPTokenType> lexemeProvider; protected String contentType; protected PHPSourceViewerConfiguration configuration; protected ISourceViewer sourceViewer; /** * Construct a new PHPAutoEditStrategy * * @param contentType * @param configuration * @param sourceViewer */ public AbstractPHPAutoEditStrategy(String contentType, PHPSourceViewerConfiguration configuration, ISourceViewer sourceViewer) { this.contentType = contentType; this.configuration = configuration; this.sourceViewer = sourceViewer; } /** * Returns a cached lexeme-provider, or create and return a new one.<br> * In case the includeOtherPartitions is true, the returned lexeme list will hold lexemes from other partition types * that are located <b>above</b> the given offset. Otherwise, only the lexeme provider will only hold the lexemes in * the partition that contains the offset. * * @param document * @param offset * @param includeOtherPartitions * @return A {@link LexemeProvider} * @see #setLexemeProvider(LexemeProvider) */ protected ILexemeProvider<PHPTokenType> getLexemeProvider(IDocument document, int offset, boolean includeOtherPartitions) { if (lexemeProvider == null) { if (includeOtherPartitions) { lexemeProvider = ParsingUtils.createLexemeProvider(document, Math.max(0, offset - MAX_CHARS_TO_LEX_BACK), offset); } else { lexemeProvider = ParsingUtils.createLexemeProvider(document, offset); } } return lexemeProvider; } /** * Set a lexeme-provider for use with this auto-edit strategy class.<br> * This method can be called from other auto-edit strategies to avoid the expensive re-computation of the lexemes in * case we already have them in hand. * * @param lexemeProvider * @see #getLexemeProvider(IDocument) */ protected void setLexemeProvider(ILexemeProvider<PHPTokenType> lexemeProvider) { this.lexemeProvider = lexemeProvider; } /** * @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 (command.text == null || command.length > 0 || !isAutoIndentEnabled()) { return; } String[] lineDelimiters = document.getLegalLineDelimiters(); int index = TextUtilities.endsWith(lineDelimiters, command.text); if (index > -1) { // ends with line delimiter if (lineDelimiters[index].equals(command.text)) { indentAfterNewLine(document, command); } return; } // // todo: ensure we actually need this here // else if (command.text.equals("\t")) //$NON-NLS-1$ // { // if (configuration instanceof UnifiedConfiguration) // { // UnifiedConfiguration uc = (UnifiedConfiguration) configuration; // if (uc.useSpacesAsTabs()) // { // command.text = uc.getTabAsSpaces(); // } // } // } else if (command.text.length() == 1 && isAutoInsertCharacter(command.text.charAt(0)) && isAutoInsertEnabled() && isValidAutoInsertLocation(document, command)) { char current = command.text.charAt(0); if (overwriteBracket(current, document, command)) { return; } } } /** * Match the indentation of the inserted keyword to a previous keyword. This methods traverse the lines up one by * one and look into the first lexeme at each line. It stops and add indentation to the buffer when it hits a type * that matches an item from the sameIntentItems list or the lowerIndentItems list. * * @param document * @param indentationBuffer * @param lineNumber * @param offset * @param sameIndentItems * - a set of items that are defined to be on the same level of indentation. * @param lowerIndentItems * - a set of items that are defined to be one level of indentation less. * @throws BadLocationException */ protected void matchIndent(IDocument document, StringBuilder indentationBuffer, int lineNumber, int offset, Set<String> sameIndentItems, Set<String> lowerIndentItems) throws BadLocationException { IRegion currentLineInfo = document.getLineInformationOfOffset(offset); int lineStartOffset = currentLineInfo.getOffset(); if (lineStartOffset == 0) { return; } do { currentLineInfo = document.getLineInformationOfOffset(lineStartOffset - 1); lineStartOffset = currentLineInfo.getOffset(); Lexeme<PHPTokenType> firstLexemeInLine = getFirstLexemeInLine(document, lexemeProvider, lineStartOffset); if (firstLexemeInLine != null) { String type = firstLexemeInLine.getType().getType(); if (TERMINATORS.contains(type)) { return; } // We check for lexeme that ends a block (either a closing bracket or an endxxx lexeme), we have // to find the matching lexeme and skip the entire section of lexemes in between. String pairToFind = null; if ("}".equals(firstLexemeInLine.getText())) { //$NON-NLS-1$ pairToFind = "{"; //$NON-NLS-1$ } else { pairToFind = getLexemePair(firstLexemeInLine); } // We can do this section of code only if we are certain that we are on the same level if (pairToFind == null) { if (sameIndentItems != null && sameIndentItems.contains(type)) { indentationBuffer .append(getIndentationAtOffset(document, firstLexemeInLine.getStartingOffset())); return; } if (lowerIndentItems != null && lowerIndentItems.contains(type)) { String indent = getIndentationAtOffset(document, firstLexemeInLine.getStartingOffset()); indent += configuration.getIndent(); // $codepro.audit.disable stringConcatenationInLoop indentationBuffer.append(indent); return; } } else { // Do a detailed scan lexeme-by-lexeme until we find a match. // In case we can't find any, -1 is returned and will cause this loop to end. lineStartOffset = getPervPairMatchOffset(pairToFind, currentLineInfo.getOffset(), document); if (lineStartOffset > 0) { lineStartOffset++; } } } } while (lineStartOffset > 0); } /** * Do a more precise scan backwards for the opening match of the given pair string. * * @param pairToFind * @param offset * - The offset to start scanning from (The scan is done backward) * @param document * @return the offset of the start line we found the pair at; -1, in case we could not locate the match * @throws BadLocationException */ protected int getPervPairMatchOffset(String pairToFind, int offset, IDocument document) throws BadLocationException { // We have to maintain a stack of elements. There is always a chance that we have more blocks in our search // scope. Stack<String> stack = new Stack<String>(); stack.push(pairToFind); Lexeme<PHPTokenType> lexeme = lexemeProvider.getFloorLexeme(offset); while (!stack.isEmpty() && lexeme != null && lexeme.getStartingOffset() > 0) { String type = lexeme.getType().getType(); String nextToMatch = stack.peek(); // there is always something in this stack if (nextToMatch.equals(type) || nextToMatch.equals(lexeme.getText())) { // found a match! // We just need to check we have only one item in the stack to be certain we got the pair if (stack.size() == 1) { IRegion lexemeLine = document.getLineInformationOfOffset(lexeme.getStartingOffset()); return lexemeLine.getOffset(); } else { // just pop an element stack.pop(); } } else { // we need to check if we got another block ending here if ("}".equals(lexeme.getText())) { //$NON-NLS-1$ stack.push("{"); //$NON-NLS-1$ } else { String pair = getLexemePair(lexeme); if (pair != null) { stack.push(pair); } } } lexeme = lexemeProvider.getFloorLexeme(lexeme.getStartingOffset() - 1); } return -1; } protected Lexeme<PHPTokenType> getPreviousNonWhitespaceLexeme(int offset) { int index = lexemeProvider.getLexemeFloorIndex(offset); Lexeme<PHPTokenType> lexeme = lexemeProvider.getLexeme(index); while (lexeme != null && PHPRegionTypes.WHITESPACE.equals(lexeme.getType().getType()) && index > 0) { index--; lexeme = lexemeProvider.getLexeme(index); } if (lexeme != null && !PHPRegionTypes.WHITESPACE.equals(lexeme.getType().getType())) { return lexeme; } return null; } /** * Returns the open pair type for the given lexeme (in case it represents a closing element) * * @param lexeme * @return An open element type, or null. */ protected String getLexemePair(Lexeme<PHPTokenType> lexeme) { return PAIR_MATCH.get(lexeme.getType().getType()); } /** * isValidAutoInsertLocation * * @param d * @param c * @return boolean */ protected boolean isValidAutoInsertLocation(IDocument document, DocumentCommand command) { return true; } /** * overwriteBracket * * @param bracket * @param document * @param command * @param ll * @return boolean */ public boolean overwriteBracket(char bracket, IDocument document, DocumentCommand command) { // if next character is "closing" char, overwrite if (canOverwriteBracket(bracket, command.offset, document)) { command.text = StringUtils.EMPTY; command.shiftsCaret = false; command.caretOffset = command.offset + 1; return true; } return false; } /** * Returns the first, non-whitespace, lexeme in the line of the given offset. * * @param document * @param lexemeProvider * @param startingOffset * @return The first non-whitespace lexeme in the given line. Null, if none is found. * @throws BadLocationException */ protected Lexeme<PHPTokenType> getFirstLexemeInLine(IDocument document, ILexemeProvider<PHPTokenType> lexemeProvider, int offset) throws BadLocationException { if (offset < 0) { return null; } IRegion lineRegion = document.getLineInformationOfOffset(offset); Lexeme<PHPTokenType> lexeme = lexemeProvider.getCeilingLexeme(lineRegion.getOffset()); if (lexeme == null || !PHPRegionTypes.WHITESPACE.equals(lexeme.getType().getType())) { return lexeme; } // The first non-whitespace lexeme should be on our right int index = lexemeProvider.getLexemeIndex(lexeme.getStartingOffset()); if (index + 1 < lexemeProvider.size()) { lexeme = lexemeProvider.getLexeme(index + 1); if (lexeme.getStartingOffset() < lineRegion.getOffset() + lineRegion.getLength()) { return lexeme; } } return null; } /** * Returns the last, non-whitespace, lexeme in the line of the given offset. * * @param document * @param lexemeProvider * @param startingOffset * @return The last non-whitespace lexeme in the given line. Null, if none is found. * @throws BadLocationException */ protected Lexeme<PHPTokenType> getLastLexemeInLine(IDocument document, ILexemeProvider<PHPTokenType> lexemeProvider, int offset) throws BadLocationException { IRegion lineRegion = document.getLineInformationOfOffset(offset); int lastCharInLine = lineRegion.getOffset() + lineRegion.getLength(); if (lineRegion.getLength() > 0) { lastCharInLine--; } Lexeme<PHPTokenType> lexeme = lexemeProvider.getFloorLexeme(lastCharInLine); if (lexeme == null || !PHPRegionTypes.WHITESPACE.equals(lexeme.getType().getType())) { return lexeme; } // The first non-whitespace lexeme should be on our left int index = lexemeProvider.getLexemeIndex(lexeme.getStartingOffset()); if (index - 1 > 0) { lexeme = lexemeProvider.getLexeme(index - 1); if (lexeme.getStartingOffset() > lineRegion.getOffset()) { return lexeme; } } return null; } /** * Returns the first, non-whitespace, lexeme in the first non-empty line <b>above</b> the line at the given offset. * * @param document * @param lexemeProvider * @param startingOffset * @return The first non-whitespace lexeme in the first, non-empty, line above the offset. * @throws BadLocationException */ protected Lexeme<PHPTokenType> getFirstLexemeInNonEmptyLine(IDocument document, ILexemeProvider<PHPTokenType> lexemeProvider, int offset) throws BadLocationException { IRegion lineInfo = null; Lexeme<PHPTokenType> lexeme = null; do { lineInfo = document.getLineInformationOfOffset(offset); lexeme = getFirstLexemeInLine(document, lexemeProvider, lineInfo.getOffset() - 1); if (lineInfo != null) { offset = lineInfo.getOffset() - 1; } } while (lexeme == null && lineInfo != null && lineInfo.getOffset() > 0 && offset > 0); return lexeme; } /** * canOverwriteBracket * * @param bracket * @param offset * @param document * @param ll * @return boolean */ public boolean canOverwriteBracket(char bracket, int offset, IDocument document) { if (offset < document.getLength()) { char[] autoOverwriteChars = getAutoOverwriteCharacters(); Arrays.sort(autoOverwriteChars); if (Arrays.binarySearch(autoOverwriteChars, bracket) < 0) { return false; } // If the next char is a ">", our tag is already closed try { char sibling = document.getChar(offset); return sibling == bracket; } catch (BadLocationException ex) { return false; } } return false; } /** * getAutoOverwriteCharacters * * @return char[] */ protected char[] getAutoOverwriteCharacters() { return new char[] { ')', '>', ']', '"', '\'', '}' }; } /** * Returns the preference value of the auto insert. * * @return True by default. */ protected boolean isAutoInsertEnabled() { // TODO: Shalom Attach this to the php/studio preferences. return true; } /** * Returns the preference value of auto insert indents */ protected boolean isAutoIndentEnabled() { return PHPEplPlugin.getDefault().getPreferenceStore().getBoolean(IPreferenceConstants.EDITOR_AUTO_INDENT); } /** * indentAfterNewLine * * @param d * @param c */ protected void indentAfterNewLine(IDocument d, DocumentCommand c) { String indentString = configuration.getIndent(); // nothing to add if nothing to add if (indentString.equals(StringUtils.EMPTY)) { return; } int offset = c.offset; if (offset == -1 || d.getLength() == 0) { return; } c.text += getIndentationAtOffset(d, offset); return; } /** * getIndentationAtOffset * * @param d * @param offset * @return String */ protected String getIndentationAtOffset(IDocument d, int offset) { String indentation = StringUtils.EMPTY; try { int p = ((offset == d.getLength()) ? offset - 1 : offset); IRegion line = d.getLineInformationOfOffset(p); int lineOffset = line.getOffset(); int firstNonWS = findEndOfWhiteSpace(d, lineOffset, offset); indentation = getIndentationString(d, lineOffset, firstNonWS); } catch (BadLocationException excp) { IdeLog.logWarning( PHPEditorPlugin.getDefault(), "PHP Auto Edit Strategy - Bad location while computing the indentation at offset (getIndentationAtOffset)", //$NON-NLS-1$ excp, PHPEditorPlugin.DEBUG_SCOPE); } return indentation; } protected int findEndOfWhiteSpace(IDocument document, int offset, int end) throws BadLocationException { while (offset < end) { char c = document.getChar(offset); if (c != ' ' && c != '\t') { return offset; } offset++; } return end; } /** * @param c * @return */ private boolean isAutoInsertCharacter(char c) { int val = Arrays.binarySearch(getAutoInsertCharacters(), c); return val >= 0; } /** * getAutoInsertCharacters * * @return char[] */ protected char[] getAutoInsertCharacters() { return new char[] { '(', '<', '[', '"', '\'', '{' }; } /** * Calculates the whitespace prefix based on user prefs and the existing line. Eg: if the line prefix is five * spaces, and user pref is tabs of width 4, then the result is "/t ". * * @param d * @param lineOffset * @param firstNonWS * @return Returns the whitespace prefix based on user prefs and the existing line. */ protected String getIndentationString(IDocument d, int lineOffset, int firstNonWS) { String lineIndent = StringUtils.EMPTY; try { lineIndent = d.get(lineOffset, firstNonWS - lineOffset); } catch (BadLocationException e1) { IdeLog.logWarning(PHPEditorPlugin.getDefault(), "PHP Auto Edit Strategy - Bad location while computing a line indentation (getIndentationString)", //$NON-NLS-1$ e1, PHPEditorPlugin.DEBUG_SCOPE); } if (lineIndent.equals(StringUtils.EMPTY)) { return lineIndent; } int indentSize = 0; int tabWidth = Math.max(1, this.configuration.getTabWidth(sourceViewer)); char[] indentChars = lineIndent.toCharArray(); for (int i = 0; i < indentChars.length; i++) { char e = indentChars[i]; if (e == '\t') { indentSize += tabWidth - (indentSize % tabWidth); } else { indentSize++; } } String indentString = configuration.getIndent(); int indentStringWidth = (indentString.equals("\t")) ? tabWidth : indentString.length(); //$NON-NLS-1$ // return in case tab width is zero if (indentStringWidth == 0) { return StringUtils.EMPTY; } int indentCount = (int) Math.floor(indentSize / indentStringWidth); // assume no dived by zero from above tests StringBuilder indentation = new StringBuilder(); for (int i = 0; i < indentCount; i++) { indentation.append(indentString); } // here we might want to allow one tab when there are three spaces on the previous line when tabwdith = 4 // logic is just get the ending from the previous line int extra = indentSize % indentStringWidth; indentation.append(spaces.substring(0, extra));// lineIndent.substring(lineIndent.length() - extra); return indentation.toString(); } }