X-Git-Url: http://secure.phpeclipse.com diff --git a/net.sourceforge.phpeclipse/src/net/sourceforge/phpdt/internal/ui/text/SmartSemicolonAutoEditStrategy.java b/net.sourceforge.phpeclipse/src/net/sourceforge/phpdt/internal/ui/text/SmartSemicolonAutoEditStrategy.java new file mode 100644 index 0000000..d0b0d8d --- /dev/null +++ b/net.sourceforge.phpeclipse/src/net/sourceforge/phpdt/internal/ui/text/SmartSemicolonAutoEditStrategy.java @@ -0,0 +1,1300 @@ +/******************************************************************************* + * Copyright (c) 2000, 2004 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Common Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/cpl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package net.sourceforge.phpdt.internal.ui.text; + +import java.util.Arrays; + +import net.sourceforge.phpdt.internal.compiler.parser.Scanner; +//incastrix +//import net.sourceforge.phpdt.internal.corext.Assert; +import org.eclipse.core.runtime.Assert; +import net.sourceforge.phpdt.internal.ui.text.SmartBackspaceManager.UndoSpec; +import net.sourceforge.phpdt.ui.PreferenceConstants; +import net.sourceforge.phpeclipse.PHPeclipsePlugin; +import net.sourceforge.phpeclipse.phpeditor.PHPUnitEditor; + +import org.eclipse.jface.preference.IPreferenceStore; +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.ITextSelection; +import org.eclipse.jface.text.ITypedRegion; +import org.eclipse.jface.text.Region; +import org.eclipse.jface.text.TextSelection; +import org.eclipse.jface.text.TextUtilities; +import org.eclipse.text.edits.DeleteEdit; +import org.eclipse.text.edits.MalformedTreeException; +import org.eclipse.text.edits.ReplaceEdit; +import org.eclipse.text.edits.TextEdit; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.texteditor.ITextEditorExtension2; +import org.eclipse.ui.texteditor.ITextEditorExtension3; + +/** + * Modifies DocumentCommands inserting semicolons and opening + * braces to place them smartly, i.e. moving them to the end of a line if that + * is what the user expects. + * + *

+ * In practice, semicolons and braces (and the caret) are moved to the end of + * the line if they are typed anywhere except for semicolons in a + * for statements definition. If the line contains a semicolon or + * brace after the current caret position, the cursor is moved after it. + *

+ * + * @see org.eclipse.jface.text.DocumentCommand + * @since 3.0 + */ +public class SmartSemicolonAutoEditStrategy implements IAutoEditStrategy { + + /** String representation of a semicolon. */ + private static final String SEMICOLON = ";"; //$NON-NLS-1$ + + /** Char representation of a semicolon. */ + private static final char SEMICHAR = ';'; + + /** String represenattion of a opening brace. */ + private static final String BRACE = "{"; //$NON-NLS-1$ + + /** Char representation of a opening brace */ + private static final char BRACECHAR = '{'; + + private char fCharacter; + + private String fPartitioning; + + /** + * Creates a new SmartSemicolonAutoEditStrategy. + * + * @param partitioning + * the document partitioning + */ + public SmartSemicolonAutoEditStrategy(String partitioning) { + fPartitioning = partitioning; + } + + /* + * @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument, + * org.eclipse.jface.text.DocumentCommand) + */ + public void customizeDocumentCommand(IDocument document, + DocumentCommand command) { + // 0: early pruning + // also customize if doit is false (so it works in code + // completion situations) + // if (!command.doit) + // return; + + if (command.text == null) + return; + + if (command.text.equals(SEMICOLON)) + fCharacter = SEMICHAR; + else if (command.text.equals(BRACE)) + fCharacter = BRACECHAR; + else + return; + + IPreferenceStore store = PHPeclipsePlugin.getDefault() + .getPreferenceStore(); + if (fCharacter == SEMICHAR + && !store + .getBoolean(PreferenceConstants.EDITOR_SMART_SEMICOLON)) + return; + if (fCharacter == BRACECHAR + && !store + .getBoolean(PreferenceConstants.EDITOR_SMART_OPENING_BRACE)) + return; + + IWorkbenchPage page = PHPeclipsePlugin.getActivePage(); + if (page == null) + return; + IEditorPart part = page.getActiveEditor(); + if (!(part instanceof PHPUnitEditor)) + return; + PHPUnitEditor editor = (PHPUnitEditor) part; + if (editor.getInsertMode() != ITextEditorExtension3.SMART_INSERT + || !editor.isEditable()) + return; + ITextEditorExtension2 extension = (ITextEditorExtension2) editor + .getAdapter(ITextEditorExtension2.class); + if (extension != null && !extension.validateEditorInputState()) + return; + if (isMultilineSelection(document, command)) + return; + + // 1: find concerned line / position in java code, location in statement + int pos = command.offset; + ITextSelection line; + try { + IRegion l = document.getLineInformationOfOffset(pos); + line = new TextSelection(document, l.getOffset(), l.getLength()); + } catch (BadLocationException e) { + return; + } + + // 2: choose action based on findings (is for-Statement?) + // for now: compute the best position to insert the new character + int positionInLine = computeCharacterPosition(document, line, pos + - line.getOffset(), fCharacter, fPartitioning); + int position = positionInLine + line.getOffset(); + + // never position before the current position! + if (position < pos) + return; + + // never double already existing content + if (alreadyPresent(document, fCharacter, position)) + return; + + // don't do special processing if what we do is actually the normal + // behaviour + String insertion = adjustSpacing(document, position, fCharacter); + if (command.offset == position && insertion.equals(command.text)) + return; + + try { + + final SmartBackspaceManager manager = (SmartBackspaceManager) editor + .getAdapter(SmartBackspaceManager.class); + if (manager != null + && PHPeclipsePlugin.getDefault().getPreferenceStore() + .getBoolean( + PreferenceConstants.EDITOR_SMART_BACKSPACE)) { + TextEdit e1 = new ReplaceEdit(command.offset, command.text + .length(), document.get(command.offset, command.length)); + UndoSpec s1 = new UndoSpec(command.offset + + command.text.length(), new Region(command.offset, 0), + new TextEdit[] { e1 }, 0, null); + + DeleteEdit smart = new DeleteEdit(position, insertion.length()); + ReplaceEdit raw = new ReplaceEdit(command.offset, + command.length, command.text); + UndoSpec s2 = new UndoSpec(position + insertion.length(), + new Region(command.offset + command.text.length(), 0), + new TextEdit[] { smart, raw }, 2, s1); + manager.register(s2); + } + + // 3: modify command + command.offset = position; + command.length = 0; + command.caretOffset = position; + command.text = insertion; + command.doit = true; + command.owner = null; + } catch (MalformedTreeException e) { + PHPeclipsePlugin.log(e); + } catch (BadLocationException e) { + PHPeclipsePlugin.log(e); + } + + } + + /** + * Returns true if the document command is applied on a multi + * line selection, false otherwise. + * + * @param document + * the document + * @param command + * the command + * @return true if command is a multiline + * command + */ + private boolean isMultilineSelection(IDocument document, + DocumentCommand command) { + try { + return document.getNumberOfLines(command.offset, command.length) > 1; + } catch (BadLocationException e) { + // ignore + return false; + } + } + + /** + * Adds a space before a brace if it is inserted after a parenthesis, equal + * sign, or one of the keywords try, else, do. + * + * @param document + * the document we are working on + * @param position + * the insert position of character + * @param character + * the character to be inserted + * @return a String consisting of character + * plus any additional spacing + */ + private String adjustSpacing(IDocument doc, int position, char character) { + if (character == BRACECHAR) { + if (position > 0 && position <= doc.getLength()) { + int pos = position - 1; + if (looksLike(doc, pos, ")") //$NON-NLS-1$ + || looksLike(doc, pos, "=") //$NON-NLS-1$ + || looksLike(doc, pos, "]") //$NON-NLS-1$ + || looksLike(doc, pos, "try") //$NON-NLS-1$ + || looksLike(doc, pos, "else") //$NON-NLS-1$ + || looksLike(doc, pos, "synchronized") //$NON-NLS-1$ + || looksLike(doc, pos, "static") //$NON-NLS-1$ + || looksLike(doc, pos, "finally") //$NON-NLS-1$ + || looksLike(doc, pos, "do")) //$NON-NLS-1$ + return new String(new char[] { ' ', character }); + } + } + + return new String(new char[] { character }); + } + + /** + * Checks whether a character to be inserted is already present at the + * insert location (perhaps separated by some whitespace from + * position. + * + * @param document + * the document we are working on + * @param position + * the insert position of ch + * @param character + * the character to be inserted + * @return true if ch is already present at + * location, false otherwise + */ + private boolean alreadyPresent(IDocument document, char ch, int position) { + int pos = firstNonWhitespaceForward(document, position, fPartitioning, + document.getLength()); + try { + if (pos != -1 && document.getChar(pos) == ch) + return true; + } catch (BadLocationException e) { + } + + return false; + } + + /** + * Computes the next insert position of the given character in the current + * line. + * + * @param document + * the document we are working on + * @param line + * the line where the change is being made + * @param offset + * the position of the caret in the line when + * character was typed + * @param character + * the character to look for + * @param partitioning + * the document partitioning + * @return the position where character should be inserted / + * replaced + */ + protected static int computeCharacterPosition(IDocument document, + ITextSelection line, int offset, char character, String partitioning) { + String text = line.getText(); + if (text == null) + return 0; + + int insertPos; + if (character == BRACECHAR) { + + insertPos = computeArrayInitializationPos(document, line, offset, + partitioning); + + if (insertPos == -1) { + insertPos = computeAfterTryDoElse(document, line, offset); + } + + if (insertPos == -1) { + insertPos = computeAfterParenthesis(document, line, offset, + partitioning); + } + + } else if (character == SEMICHAR) { + + if (isForStatement(text, offset)) { + insertPos = -1; // don't do anything in for statements, as semis + // are vital part of these + } else { + int nextPartitionPos = nextPartitionOrLineEnd(document, line, + offset, partitioning); + insertPos = startOfWhitespaceBeforeOffset(text, + nextPartitionPos); + // if there is a semi present, return its location as + // alreadyPresent() will take it out this way. + if (insertPos > 0 && text.charAt(insertPos - 1) == character) + insertPos = insertPos - 1; + } + + } else { + Assert.isTrue(false); + return -1; + } + + return insertPos; + } + + /** + * Computes an insert position for an opening brace if offset + * maps to a position in document that looks like being the + * RHS of an assignment or like an array definition. + * + * @param document + * the document being modified + * @param line + * the current line under investigation + * @param offset + * the offset of the caret position, relative to the line start. + * @param partitioning + * the document partitioning + * @return an insert position relative to the line start if + * line looks like being an array initialization at + * offset, -1 otherwise + */ + private static int computeArrayInitializationPos(IDocument document, + ITextSelection line, int offset, String partitioning) { + // search backward while WS, find = (not != <= >= ==) in default + // partition + int pos = offset + line.getOffset(); + + if (pos == 0) + return -1; + + int p = firstNonWhitespaceBackward(document, pos - 1, partitioning, -1); + + if (p == -1) + return -1; + + try { + + char ch = document.getChar(p); + if (ch != '=' && ch != ']') + return -1; + + if (p == 0) + return offset; + + p = firstNonWhitespaceBackward(document, p - 1, partitioning, -1); + if (p == -1) + return -1; + + ch = document.getChar(p); + if (Scanner.isPHPIdentifierPart(ch) || ch == ']' || ch == '[') + return offset; + + } catch (BadLocationException e) { + } + return -1; + } + + /** + * Computes an insert position for an opening brace if offset + * maps to a position in document involving a keyword taking + * a block after it. These are: try, do, + * synchronized, static, + * finally, or else. + * + * @param document + * the document being modified + * @param line + * the current line under investigation + * @param offset + * the offset of the caret position, relative to the line start. + * @return an insert position relative to the line start if + * line contains one of the above keywords at or + * before offset, -1 otherwise + */ + private static int computeAfterTryDoElse(IDocument doc, + ITextSelection line, int offset) { + // search backward while WS, find 'try', 'do', 'else' in default + // partition + int p = offset + line.getOffset(); + p = firstWhitespaceToRight(doc, p); + if (p == -1) + return -1; + p--; + + if (looksLike(doc, p, "try") //$NON-NLS-1$ + || looksLike(doc, p, "do") //$NON-NLS-1$ + || looksLike(doc, p, "synchronized") //$NON-NLS-1$ + || looksLike(doc, p, "static") //$NON-NLS-1$ + || looksLike(doc, p, "finally") //$NON-NLS-1$ + || looksLike(doc, p, "else")) //$NON-NLS-1$ + return p + 1 - line.getOffset(); + + return -1; + } + + /** + * Computes an insert position for an opening brace if offset + * maps to a position in document with a expression in + * parenthesis that will take a block after the closing parenthesis. + * + * @param document + * the document being modified + * @param line + * the current line under investigation + * @param offset + * the offset of the caret position, relative to the line start. + * @param partitioning + * the document partitioning + * @return an insert position relative to the line start if + * line contains a parenthesized expression that can + * be followed by a block, -1 otherwise + */ + private static int computeAfterParenthesis(IDocument document, + ITextSelection line, int offset, String partitioning) { + // find the opening parenthesis for every closing parenthesis on the + // current line after offset + // return the position behind the closing parenthesis if it looks like a + // method declaration + // or an expression for an if, while, for, catch statement + int pos = offset + line.getOffset(); + int length = line.getOffset() + line.getLength(); + int scanTo = scanForward(document, pos, partitioning, length, '}'); + if (scanTo == -1) + scanTo = length; + + int closingParen = findClosingParenToLeft(document, pos, partitioning) - 1; + + while (true) { + int startScan = closingParen + 1; + closingParen = scanForward(document, startScan, partitioning, + scanTo, ')'); + if (closingParen == -1) + break; + + int openingParen = findOpeningParenMatch(document, closingParen, + partitioning); + + // no way an expression at the beginning of the document can mean + // anything + if (openingParen < 1) + break; + + // only select insert positions for parenthesis currently embracing + // the caret + if (openingParen > pos) + continue; + + if (looksLikeAnonymousClassDef(document, openingParen - 1, + partitioning)) + return closingParen + 1 - line.getOffset(); + + if (looksLikeIfWhileForCatch(document, openingParen - 1, + partitioning)) + return closingParen + 1 - line.getOffset(); + + if (looksLikeMethodDecl(document, openingParen - 1, partitioning)) + return closingParen + 1 - line.getOffset(); + + } + + return -1; + } + + /** + * Finds a closing parenthesis to the left of position in + * document, where that parenthesis is only separated by whitespace from + * position. If no such parenthesis can be found, + * position is returned. + * + * @param document + * the document being modified + * @param position + * the first character position in document to be + * considered + * @param partitioning + * the document partitioning + * @return the position of a closing parenthesis left to + * position separated only by whitespace, or + * position if no parenthesis can be found + */ + private static int findClosingParenToLeft(IDocument document, int position, + String partitioning) { + final char CLOSING_PAREN = ')'; + try { + if (position < 1) + return position; + + int nonWS = firstNonWhitespaceBackward(document, position - 1, + partitioning, -1); + if (nonWS != -1 && document.getChar(nonWS) == CLOSING_PAREN) + return nonWS; + } catch (BadLocationException e1) { + } + return position; + } + + /** + * Finds the first whitespace character position to the right of (and + * including) position. + * + * @param document + * the document being modified + * @param position + * the first character position in document to be + * considered + * @return the position of a whitespace character greater or equal than + * position separated only by whitespace, or -1 if + * none found + */ + private static int firstWhitespaceToRight(IDocument document, int position) { + int length = document.getLength(); + Assert.isTrue(position >= 0); + Assert.isTrue(position <= length); + + try { + while (position < length) { + char ch = document.getChar(position); + if (Character.isWhitespace(ch)) + return position; + position++; + } + return position; + } catch (BadLocationException e) { + } + return -1; + } + + /** + * Finds the highest position in document such that the + * position is <= position and > bound + * and Character.isWhitespace(document.getChar(pos)) + * evaluates to false and the position is in the default + * partition. + * + * @param document + * the document being modified + * @param position + * the first character position in document to be + * considered + * @param partitioning + * the document partitioning + * @param bound + * the first position in document to not consider + * any more, with bound > position + * @return the highest position of one element in chars in [position, + * scanTo) that resides in a Java partition, or + * -1 if none can be found + */ + private static int firstNonWhitespaceBackward(IDocument document, + int position, String partitioning, int bound) { + Assert.isTrue(position < document.getLength()); + Assert.isTrue(bound >= -1); + + try { + while (position > bound) { + char ch = document.getChar(position); + if (!Character.isWhitespace(ch) + && isDefaultPartition(document, position, partitioning)) + return position; + position--; + } + } catch (BadLocationException e) { + } + return -1; + } + + /** + * Finds the smallest position in document such that the + * position is >= position and < bound + * and Character.isWhitespace(document.getChar(pos)) + * evaluates to false and the position is in the default + * partition. + * + * @param document + * the document being modified + * @param position + * the first character position in document to be + * considered + * @param partitioning + * the document partitioning + * @param bound + * the first position in document to not consider + * any more, with bound > position + * @return the smallest position of one element in chars in [position, + * scanTo) that resides in a Java partition, or + * -1 if none can be found + */ + private static int firstNonWhitespaceForward(IDocument document, + int position, String partitioning, int bound) { + Assert.isTrue(position >= 0); + Assert.isTrue(bound <= document.getLength()); + + try { + while (position < bound) { + char ch = document.getChar(position); + if (!Character.isWhitespace(ch) + && isDefaultPartition(document, position, partitioning)) + return position; + position++; + } + } catch (BadLocationException e) { + } + return -1; + } + + /** + * Finds the highest position in document such that the + * position is <= position and > bound + * and document.getChar(position) == ch evaluates to + * true for at least one ch in chars and the + * position is in the default partition. + * + * @param document + * the document being modified + * @param position + * the first character position in document to be + * considered + * @param partitioning + * the document partitioning + * @param bound + * the first position in document to not consider + * any more, with scanTo > + * position + * @param chars + * an array of char to search for + * @return the highest position of one element in chars in (bound, + * position] that resides in a Java partition, or + * -1 if none can be found + */ + private static int scanBackward(IDocument document, int position, + String partitioning, int bound, char[] chars) { + Assert.isTrue(bound >= -1); + Assert.isTrue(position < document.getLength()); + + Arrays.sort(chars); + + try { + while (position > bound) { + + if (Arrays.binarySearch(chars, document.getChar(position)) >= 0 + && isDefaultPartition(document, position, partitioning)) + return position; + + position--; + } + } catch (BadLocationException e) { + } + return -1; + } + + // /** + // * Finds the highest position in document such that the + // position is <= position + // * and > bound and document.getChar(position) == + // ch evaluates to true + // * and the position is in the default partition. + // * + // * @param document the document being modified + // * @param position the first character position in document + // to be considered + // * @param bound the first position in document to not + // consider any more, with scanTo > position + // * @param chars an array of char to search for + // * @return the highest position of one element in chars in + // [position, scanTo) that resides in a Java + // partition, or -1 if none can be found + // */ + // private static int scanBackward(IDocument document, int position, int + // bound, char ch) { + // return scanBackward(document, position, bound, new char[] {ch}); + // } + // + /** + * Finds the lowest position in document such that the + * position is >= position and < bound + * and document.getChar(position) == ch evaluates to + * true for at least one ch in chars and the + * position is in the default partition. + * + * @param document + * the document being modified + * @param position + * the first character position in document to be + * considered + * @param partitioning + * the document partitioning + * @param bound + * the first position in document to not consider + * any more, with scanTo > + * position + * @param chars + * an array of char to search for + * @return the lowest position of one element in chars in [position, + * bound) that resides in a Java partition, or + * -1 if none can be found + */ + private static int scanForward(IDocument document, int position, + String partitioning, int bound, char[] chars) { + Assert.isTrue(position >= 0); + Assert.isTrue(bound <= document.getLength()); + + Arrays.sort(chars); + + try { + while (position < bound) { + + if (Arrays.binarySearch(chars, document.getChar(position)) >= 0 + && isDefaultPartition(document, position, partitioning)) + return position; + + position++; + } + } catch (BadLocationException e) { + } + return -1; + } + + /** + * Finds the lowest position in document such that the + * position is >= position and < bound + * and document.getChar(position) == ch evaluates to + * true and the position is in the default partition. + * + * @param document + * the document being modified + * @param position + * the first character position in document to be + * considered + * @param partitioning + * the document partitioning + * @param bound + * the first position in document to not consider + * any more, with scanTo > + * position + * @param chars + * an array of char to search for + * @return the lowest position of one element in chars in [position, + * bound) that resides in a Java partition, or + * -1 if none can be found + */ + private static int scanForward(IDocument document, int position, + String partitioning, int bound, char ch) { + return scanForward(document, position, partitioning, bound, + new char[] { ch }); + } + + /** + * Checks whether the content of document in the range (offset, + * length) contains the new keyword. + * + * @param document + * the document being modified + * @param offset + * the first character position in document to be + * considered + * @param length + * the length of the character range to be considered + * @param partitioning + * the document partitioning + * @return true if the specified character range contains a + * new keyword, false otherwise. + */ + private static boolean isNewMatch(IDocument document, int offset, + int length, String partitioning) { + Assert.isTrue(length >= 0); + Assert.isTrue(offset >= 0); + Assert.isTrue(offset + length < document.getLength() + 1); + + try { + String text = document.get(offset, length); + int pos = text.indexOf("new"); //$NON-NLS-1$ + + while (pos != -1 + && !isDefaultPartition(document, pos + offset, partitioning)) + pos = text.indexOf("new", pos + 2); //$NON-NLS-1$ + + if (pos < 0) + return false; + + if (pos != 0 && Scanner.isPHPIdentifierPart(text.charAt(pos - 1))) + return false; + + if (pos + 3 < length + && Scanner.isPHPIdentifierPart(text.charAt(pos + 3))) + return false; + + return true; + + } catch (BadLocationException e) { + } + return false; + } + + /** + * Checks whether the content of document at + * position looks like an anonymous class definition. + * position must be to the left of the opening parenthesis of + * the definition's parameter list. + * + * @param document + * the document being modified + * @param position + * the first character position in document to be + * considered + * @param partitioning + * the document partitioning + * @return true if the content of document + * looks like an anonymous class definition, false + * otherwise + */ + private static boolean looksLikeAnonymousClassDef(IDocument document, + int position, String partitioning) { + int previousCommaOrParen = scanBackward(document, position - 1, + partitioning, -1, new char[] { ',', '(' }); + if (previousCommaOrParen == -1 || position < previousCommaOrParen + 5) // 2 + // for + // borders, + // 3 + // for + // "new" + return false; + + if (isNewMatch(document, previousCommaOrParen + 1, position + - previousCommaOrParen - 2, partitioning)) + return true; + + return false; + } + + /** + * Checks whether position resides in a default (Java) + * partition of document. + * + * @param document + * the document being modified + * @param position + * the position to be checked + * @param partitioning + * the document partitioning + * @return true if position is in the default + * partition of document, false + * otherwise + */ + private static boolean isDefaultPartition(IDocument document, int position, + String partitioning) { + Assert.isTrue(position >= 0); + Assert.isTrue(position <= document.getLength()); + + try { + // don't use getPartition2 since we're interested in the scanned + // character's partition + ITypedRegion region = TextUtilities.getPartition(document, + partitioning, position, false); + return region.getType().equals(IDocument.DEFAULT_CONTENT_TYPE); + + } catch (BadLocationException e) { + } + + return false; + } + + /** + * Finds the position of the parenthesis matching the closing parenthesis at + * position. + * + * @param document + * the document being modified + * @param position + * the position in document of a closing + * parenthesis + * @param partitioning + * the document partitioning + * @return the position in document of the matching + * parenthesis, or -1 if none can be found + */ + private static int findOpeningParenMatch(IDocument document, int position, + String partitioning) { + final char CLOSING_PAREN = ')'; + final char OPENING_PAREN = '('; + + Assert.isTrue(position < document.getLength()); + Assert.isTrue(position >= 0); + Assert.isTrue(isDefaultPartition(document, position, partitioning)); + + try { + + Assert.isTrue(document.getChar(position) == CLOSING_PAREN); + + int depth = 1; + while (true) { + position = scanBackward(document, position - 1, partitioning, + -1, new char[] { CLOSING_PAREN, OPENING_PAREN }); + if (position == -1) + return -1; + + if (document.getChar(position) == CLOSING_PAREN) + depth++; + else + depth--; + + if (depth == 0) + return position; + } + + } catch (BadLocationException e) { + return -1; + } + } + + /** + * Checks whether, to the left of position and separated only + * by whitespace, document contains a keyword taking a + * parameter list and a block after it. These are: if, + * while, catch, for, + * synchronized, switch. + * + * @param document + * the document being modified + * @param position + * the first character position in document to be + * considered + * @param partitioning + * the document partitioning + * @return true if document contains any of + * the above keywords to the left of position, + * false otherwise + */ + private static boolean looksLikeIfWhileForCatch(IDocument document, + int position, String partitioning) { + position = firstNonWhitespaceBackward(document, position, partitioning, + -1); + if (position == -1) + return false; + + return looksLike(document, position, "if") //$NON-NLS-1$ + || looksLike(document, position, "while") //$NON-NLS-1$ + || looksLike(document, position, "catch") //$NON-NLS-1$ + || looksLike(document, position, "synchronized") //$NON-NLS-1$ + || looksLike(document, position, "switch") //$NON-NLS-1$ + || looksLike(document, position, "for"); //$NON-NLS-1$ + } + + /** + * Checks whether code>document contains the String like + * such that its last character is at position. If like + * starts with a identifier part (as determined by + * {@link Scanner#isPHPIdentifierPart(char)}), it is also made sure that + * like is preceded by some non-identifier character or + * stands at the document start. + * + * @param document + * the document being modified + * @param position + * the first character position in document to be + * considered + * @param like + * the String to look for. + * @return true if document contains like + * such that it ends at position, false + * otherwise + */ + private static boolean looksLike(IDocument document, int position, + String like) { + int length = like.length(); + if (position < length - 1) + return false; + + try { + if (!like.equals(document.get(position - length + 1, length))) + return false; + + if (position >= length + && Scanner.isPHPIdentifierPart(like.charAt(0)) + && Scanner.isPHPIdentifierPart(document.getChar(position + - length))) + return false; + + } catch (BadLocationException e) { + return false; + } + + return true; + } + + /** + * Checks whether the content of document at + * position looks like a method declaration header (i.e. only + * the return type and method name). position must be just + * left of the opening parenthesis of the parameter list. + * + * @param document + * the document being modified + * @param position + * the first character position in document to be + * considered + * @param partitioning + * the document partitioning + * @return true if the content of document + * looks like a method definition, false otherwise + */ + private static boolean looksLikeMethodDecl(IDocument document, + int position, String partitioning) { + + // method name + position = eatIdentToLeft(document, position, partitioning); + if (position < 1) + return false; + + position = eatBrackets(document, position - 1, partitioning); + if (position < 1) + return false; + + position = eatIdentToLeft(document, position - 1, partitioning); + + return position != -1; + } + + /** + * From position to the left, eats any whitespace and then a + * pair of brackets as used to declare an array return type like + * + *
+	 * String [ ]
+	 * 
. The return value is either the position of the opening bracket + * or position if no pair of brackets can be parsed. + * + * @param document + * the document being modified + * @param position + * the first character position in document to be + * considered + * @param partitioning + * the document partitioning + * @return the smallest character position of bracket pair or + * position + */ + private static int eatBrackets(IDocument document, int position, + String partitioning) { + // accept array return type + int pos = firstNonWhitespaceBackward(document, position, partitioning, + -1); + try { + if (pos > 1 && document.getChar(pos) == ']') { + pos = firstNonWhitespaceBackward(document, pos - 1, + partitioning, -1); + if (pos > 0 && document.getChar(pos) == '[') + return pos; + } + } catch (BadLocationException e) { + // won't happen + } + return position; + } + + /** + * From position to the left, eats any whitespace and the + * first identifier, returning the position of the first identifier + * character (in normal read order). + *

+ * When called on a document with content " some string " and + * positionition 13, the return value will be 6 (the first letter in + * string). + *

+ * + * @param document + * the document being modified + * @param position + * the first character position in document to be + * considered + * @param partitioning + * the document partitioning + * @return the smallest character position of an identifier or -1 if none + * can be found; always <= position + */ + private static int eatIdentToLeft(IDocument document, int position, + String partitioning) { + if (position < 0) + return -1; + Assert.isTrue(position < document.getLength()); + + int p = firstNonWhitespaceBackward(document, position, partitioning, -1); + if (p == -1) + return -1; + + try { + while (p >= 0) { + + char ch = document.getChar(p); + if (Scanner.isPHPIdentifierPart(ch)) { + p--; + continue; + } + + // length must be > 0 + if (Character.isWhitespace(ch) && p != position) + return p + 1; + else + return -1; + + } + + // start of document reached + return 0; + + } catch (BadLocationException e) { + } + return -1; + } + + /** + * Returns a position in the first java partition after the last non-empty + * and non-comment partition. There is no non-whitespace from the returned + * position to the end of the partition it is contained in. + * + * @param document + * the document being modified + * @param line + * the line under investigation + * @param offset + * the caret offset into line + * @param partitioning + * the document partitioning + * @return the position of the next Java partition, or the end of + * line + */ + private static int nextPartitionOrLineEnd(IDocument document, + ITextSelection line, int offset, String partitioning) { + // run relative to document + final int docOffset = offset + line.getOffset(); + final int eol = line.getOffset() + line.getLength(); + int nextPartitionPos = eol; // init with line end + int validPosition = docOffset; + + try { + ITypedRegion partition = TextUtilities.getPartition(document, + partitioning, nextPartitionPos, true); + validPosition = getValidPositionForPartition(document, partition, + eol); + while (validPosition == -1) { + nextPartitionPos = partition.getOffset() - 1; + if (nextPartitionPos < docOffset) { + validPosition = docOffset; + break; + } + partition = TextUtilities.getPartition(document, partitioning, + nextPartitionPos, false); + validPosition = getValidPositionForPartition(document, + partition, eol); + } + } catch (BadLocationException e) { + } + + validPosition = Math.max(validPosition, docOffset); + // make relative to line + validPosition -= line.getOffset(); + return validPosition; + } + + /** + * Returns a valid insert location (except for whitespace) in + * partition or -1 if there is no valid insert location. An + * valid insert location is right after any java string or character + * partition, or at the end of a java default partition, but never behind + * maxOffset. Comment partitions or empty java partitions do + * never yield valid insert positions. + * + * @param doc + * the document being modified + * @param partition + * the current partition + * @param maxOffset + * the maximum offset to consider + * @return a valid insert location in partition, or -1 if + * there is no valid insert location + */ + private static int getValidPositionForPartition(IDocument doc, + ITypedRegion partition, int maxOffset) { + final int INVALID = -1; + + if (IPHPPartitions.PHP_PHPDOC_COMMENT.equals(partition.getType())) + return INVALID; + if (IPHPPartitions.PHP_MULTILINE_COMMENT.equals(partition.getType())) + return INVALID; + if (IPHPPartitions.PHP_SINGLELINE_COMMENT.equals(partition.getType())) + return INVALID; + + int endOffset = Math.min(maxOffset, partition.getOffset() + + partition.getLength()); + + // if (IPHPPartitions.JAVA_CHARACTER.equals(partition.getType())) + // return endOffset; + if (IPHPPartitions.PHP_STRING_DQ.equals(partition.getType())) + return endOffset; + if (IPHPPartitions.PHP_STRING_SQ.equals(partition.getType())) + return endOffset; + if (IPHPPartitions.PHP_STRING_HEREDOC.equals(partition.getType())) + return endOffset; + if (IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())) { + try { + if (doc.get(partition.getOffset(), + endOffset - partition.getOffset()).trim().length() == 0) + return INVALID; + else + return endOffset; + } catch (BadLocationException e) { + return INVALID; + } + } + // default: we don't know anything about the partition - assume valid + return endOffset; + } + + /** + * Determines whether the current line contains a for statement. Algorithm: + * any "for" word in the line is a positive, "for" contained in a string + * literal will produce a false positive. + * + * @param line + * the line where the change is being made + * @param offset + * the position of the caret + * @return true if line contains + * for, false otherwise + */ + private static boolean isForStatement(String line, int offset) { + /* searching for (^|\s)for(\s|$) */ + int forPos = line.indexOf("for"); //$NON-NLS-1$ + if (forPos != -1) { + if ((forPos == 0 || !Scanner.isPHPIdentifierPart(line + .charAt(forPos - 1))) + && (line.length() == forPos + 3 || !Scanner + .isPHPIdentifierPart(line.charAt(forPos + 3)))) + return true; + } + return false; + } + + /** + * Returns the position in text after which there comes only + * whitespace, up to offset. + * + * @param text + * the text being searched + * @param offset + * the maximum offset to search for + * @return the smallest value v such that + * text.substring(v, offset).trim() == 0 + */ + private static int startOfWhitespaceBeforeOffset(String text, int offset) { + int i = Math.min(offset, text.length()); + for (; i >= 1; i--) { + if (!Character.isWhitespace(text.charAt(i - 1))) + break; + } + return i; + } +}