1 /*******************************************************************************
 
   2  * Copyright (c) 2000, 2004 IBM Corporation and others.
 
   3  * All rights reserved. This program and the accompanying materials
 
   4  * are made available under the terms of the Common Public License v1.0
 
   5  * which accompanies this distribution, and is available at
 
   6  * http://www.eclipse.org/legal/cpl-v10.html
 
   9  *     IBM Corporation - initial API and implementation
 
  10  *******************************************************************************/
 
  11 package net.sourceforge.phpdt.internal.ui.text;
 
  13 import java.util.Arrays;
 
  15 import net.sourceforge.phpdt.internal.compiler.parser.Scanner;
 
  16 import net.sourceforge.phpdt.internal.core.Assert;
 
  17 import net.sourceforge.phpdt.internal.ui.text.SmartBackspaceManager.UndoSpec;
 
  18 import net.sourceforge.phpdt.ui.PreferenceConstants;
 
  19 //import net.sourceforge.phpeclipse.PHPeclipsePlugin;
 
  20 import net.sourceforge.phpeclipse.phpeditor.PHPUnitEditor;
 
  21 import net.sourceforge.phpeclipse.ui.WebUI;
 
  23 import org.eclipse.jface.preference.IPreferenceStore;
 
  24 import org.eclipse.jface.text.BadLocationException;
 
  25 import org.eclipse.jface.text.DocumentCommand;
 
  26 import org.eclipse.jface.text.IAutoEditStrategy;
 
  27 import org.eclipse.jface.text.IDocument;
 
  28 import org.eclipse.jface.text.IRegion;
 
  29 import org.eclipse.jface.text.ITextSelection;
 
  30 import org.eclipse.jface.text.ITypedRegion;
 
  31 import org.eclipse.jface.text.Region;
 
  32 import org.eclipse.jface.text.TextSelection;
 
  33 import org.eclipse.jface.text.TextUtilities;
 
  34 import org.eclipse.text.edits.DeleteEdit;
 
  35 import org.eclipse.text.edits.MalformedTreeException;
 
  36 import org.eclipse.text.edits.ReplaceEdit;
 
  37 import org.eclipse.text.edits.TextEdit;
 
  38 import org.eclipse.ui.IEditorPart;
 
  39 import org.eclipse.ui.IWorkbenchPage;
 
  40 import org.eclipse.ui.texteditor.ITextEditorExtension2;
 
  41 import org.eclipse.ui.texteditor.ITextEditorExtension3;
 
  44  * Modifies <code>DocumentCommand</code>s inserting semicolons and opening
 
  45  * braces to place them smartly, i.e. moving them to the end of a line if that
 
  46  * is what the user expects.
 
  49  * In practice, semicolons and braces (and the caret) are moved to the end of
 
  50  * the line if they are typed anywhere except for semicolons in a
 
  51  * <code>for</code> statements definition. If the line contains a semicolon or
 
  52  * brace after the current caret position, the cursor is moved after it.
 
  55  * @see org.eclipse.jface.text.DocumentCommand
 
  58 public class SmartSemicolonAutoEditStrategy implements IAutoEditStrategy {
 
  60         /** String representation of a semicolon. */
 
  61         private static final String SEMICOLON = ";"; //$NON-NLS-1$
 
  63         /** Char representation of a semicolon. */
 
  64         private static final char SEMICHAR = ';';
 
  66         /** String represenattion of a opening brace. */
 
  67         private static final String BRACE = "{"; //$NON-NLS-1$
 
  69         /** Char representation of a opening brace */
 
  70         private static final char BRACECHAR = '{';
 
  72         private char fCharacter;
 
  74         private String fPartitioning;
 
  77          * Creates a new SmartSemicolonAutoEditStrategy.
 
  80          *            the document partitioning
 
  82         public SmartSemicolonAutoEditStrategy(String partitioning) {
 
  83                 fPartitioning = partitioning;
 
  87          * @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument,
 
  88          *      org.eclipse.jface.text.DocumentCommand)
 
  90         public void customizeDocumentCommand(IDocument document,
 
  91                         DocumentCommand command) {
 
  93                 // also customize if <code>doit</code> is false (so it works in code
 
  94                 // completion situations)
 
  98                 if (command.text == null)
 
 101                 if (command.text.equals(SEMICOLON))
 
 102                         fCharacter = SEMICHAR;
 
 103                 else if (command.text.equals(BRACE))
 
 104                         fCharacter = BRACECHAR;
 
 108                 IPreferenceStore store = WebUI.getDefault()
 
 109                                 .getPreferenceStore();
 
 110                 if (fCharacter == SEMICHAR
 
 112                                                 .getBoolean(PreferenceConstants.EDITOR_SMART_SEMICOLON))
 
 114                 if (fCharacter == BRACECHAR
 
 116                                                 .getBoolean(PreferenceConstants.EDITOR_SMART_OPENING_BRACE))
 
 119                 IWorkbenchPage page = WebUI.getActivePage();
 
 122                 IEditorPart part = page.getActiveEditor();
 
 123                 if (!(part instanceof PHPUnitEditor))
 
 125                 PHPUnitEditor editor = (PHPUnitEditor) part;
 
 126                 if (editor.getInsertMode() != ITextEditorExtension3.SMART_INSERT
 
 127                                 || !editor.isEditable())
 
 129                 ITextEditorExtension2 extension = (ITextEditorExtension2) editor
 
 130                                 .getAdapter(ITextEditorExtension2.class);
 
 131                 if (extension != null && !extension.validateEditorInputState())
 
 133                 if (isMultilineSelection(document, command))
 
 136                 // 1: find concerned line / position in java code, location in statement
 
 137                 int pos = command.offset;
 
 140                         IRegion l = document.getLineInformationOfOffset(pos);
 
 141                         line = new TextSelection(document, l.getOffset(), l.getLength());
 
 142                 } catch (BadLocationException e) {
 
 146                 // 2: choose action based on findings (is for-Statement?)
 
 147                 // for now: compute the best position to insert the new character
 
 148                 int positionInLine = computeCharacterPosition(document, line, pos
 
 149                                 - line.getOffset(), fCharacter, fPartitioning);
 
 150                 int position = positionInLine + line.getOffset();
 
 152                 // never position before the current position!
 
 156                 // never double already existing content
 
 157                 if (alreadyPresent(document, fCharacter, position))
 
 160                 // don't do special processing if what we do is actually the normal
 
 162                 String insertion = adjustSpacing(document, position, fCharacter);
 
 163                 if (command.offset == position && insertion.equals(command.text))
 
 168                         final SmartBackspaceManager manager = (SmartBackspaceManager) editor
 
 169                                         .getAdapter(SmartBackspaceManager.class);
 
 171                                         && WebUI.getDefault().getPreferenceStore()
 
 173                                                                         PreferenceConstants.EDITOR_SMART_BACKSPACE)) {
 
 174                                 TextEdit e1 = new ReplaceEdit(command.offset, command.text
 
 175                                                 .length(), document.get(command.offset, command.length));
 
 176                                 UndoSpec s1 = new UndoSpec(command.offset
 
 177                                                 + command.text.length(), new Region(command.offset, 0),
 
 178                                                 new TextEdit[] { e1 }, 0, null);
 
 180                                 DeleteEdit smart = new DeleteEdit(position, insertion.length());
 
 181                                 ReplaceEdit raw = new ReplaceEdit(command.offset,
 
 182                                                 command.length, command.text);
 
 183                                 UndoSpec s2 = new UndoSpec(position + insertion.length(),
 
 184                                                 new Region(command.offset + command.text.length(), 0),
 
 185                                                 new TextEdit[] { smart, raw }, 2, s1);
 
 186                                 manager.register(s2);
 
 190                         command.offset = position;
 
 192                         command.caretOffset = position;
 
 193                         command.text = insertion;
 
 195                         command.owner = null;
 
 196                 } catch (MalformedTreeException e) {
 
 198                 } catch (BadLocationException e) {
 
 205          * Returns <code>true</code> if the document command is applied on a multi
 
 206          * line selection, <code>false</code> otherwise.
 
 212          * @return <code>true</code> if <code>command</code> is a multiline
 
 215         private boolean isMultilineSelection(IDocument document,
 
 216                         DocumentCommand command) {
 
 218                         return document.getNumberOfLines(command.offset, command.length) > 1;
 
 219                 } catch (BadLocationException e) {
 
 226          * Adds a space before a brace if it is inserted after a parenthesis, equal
 
 227          * sign, or one of the keywords <code>try, else, do</code>.
 
 230          *            the document we are working on
 
 232          *            the insert position of <code>character</code>
 
 234          *            the character to be inserted
 
 235          * @return a <code>String</code> consisting of <code>character</code>
 
 236          *         plus any additional spacing
 
 238         private String adjustSpacing(IDocument doc, int position, char character) {
 
 239                 if (character == BRACECHAR) {
 
 240                         if (position > 0 && position <= doc.getLength()) {
 
 241                                 int pos = position - 1;
 
 242                                 if (looksLike(doc, pos, ")") //$NON-NLS-1$
 
 243                                                 || looksLike(doc, pos, "=") //$NON-NLS-1$
 
 244                                                 || looksLike(doc, pos, "]") //$NON-NLS-1$
 
 245                                                 || looksLike(doc, pos, "try") //$NON-NLS-1$
 
 246                                                 || looksLike(doc, pos, "else") //$NON-NLS-1$
 
 247                                                 || looksLike(doc, pos, "synchronized") //$NON-NLS-1$
 
 248                                                 || looksLike(doc, pos, "static") //$NON-NLS-1$
 
 249                                                 || looksLike(doc, pos, "finally") //$NON-NLS-1$
 
 250                                                 || looksLike(doc, pos, "do")) //$NON-NLS-1$
 
 251                                         return new String(new char[] { ' ', character });
 
 255                 return new String(new char[] { character });
 
 259          * Checks whether a character to be inserted is already present at the
 
 260          * insert location (perhaps separated by some whitespace from
 
 261          * <code>position</code>.
 
 264          *            the document we are working on
 
 266          *            the insert position of <code>ch</code>
 
 268          *            the character to be inserted
 
 269          * @return <code>true</code> if <code>ch</code> is already present at
 
 270          *         <code>location</code>, <code>false</code> otherwise
 
 272         private boolean alreadyPresent(IDocument document, char ch, int position) {
 
 273                 int pos = firstNonWhitespaceForward(document, position, fPartitioning,
 
 274                                 document.getLength());
 
 276                         if (pos != -1 && document.getChar(pos) == ch)
 
 278                 } catch (BadLocationException e) {
 
 285          * Computes the next insert position of the given character in the current
 
 289          *            the document we are working on
 
 291          *            the line where the change is being made
 
 293          *            the position of the caret in the line when
 
 294          *            <code>character</code> was typed
 
 296          *            the character to look for
 
 297          * @param partitioning
 
 298          *            the document partitioning
 
 299          * @return the position where <code>character</code> should be inserted /
 
 302         protected static int computeCharacterPosition(IDocument document,
 
 303                         ITextSelection line, int offset, char character, String partitioning) {
 
 304                 String text = line.getText();
 
 309                 if (character == BRACECHAR) {
 
 311                         insertPos = computeArrayInitializationPos(document, line, offset,
 
 314                         if (insertPos == -1) {
 
 315                                 insertPos = computeAfterTryDoElse(document, line, offset);
 
 318                         if (insertPos == -1) {
 
 319                                 insertPos = computeAfterParenthesis(document, line, offset,
 
 323                 } else if (character == SEMICHAR) {
 
 325                         if (isForStatement(text, offset)) {
 
 326                                 insertPos = -1; // don't do anything in for statements, as semis
 
 327                                                                 // are vital part of these
 
 329                                 int nextPartitionPos = nextPartitionOrLineEnd(document, line,
 
 330                                                 offset, partitioning);
 
 331                                 insertPos = startOfWhitespaceBeforeOffset(text,
 
 333                                 // if there is a semi present, return its location as
 
 334                                 // alreadyPresent() will take it out this way.
 
 335                                 if (insertPos > 0 && text.charAt(insertPos - 1) == character)
 
 336                                         insertPos = insertPos - 1;
 
 340                         Assert.isTrue(false);
 
 348          * Computes an insert position for an opening brace if <code>offset</code>
 
 349          * maps to a position in <code>document</code> that looks like being the
 
 350          * RHS of an assignment or like an array definition.
 
 353          *            the document being modified
 
 355          *            the current line under investigation
 
 357          *            the offset of the caret position, relative to the line start.
 
 358          * @param partitioning
 
 359          *            the document partitioning
 
 360          * @return an insert position relative to the line start if
 
 361          *         <code>line</code> looks like being an array initialization at
 
 362          *         <code>offset</code>, -1 otherwise
 
 364         private static int computeArrayInitializationPos(IDocument document,
 
 365                         ITextSelection line, int offset, String partitioning) {
 
 366                 // search backward while WS, find = (not != <= >= ==) in default
 
 368                 int pos = offset + line.getOffset();
 
 373                 int p = firstNonWhitespaceBackward(document, pos - 1, partitioning, -1);
 
 380                         char ch = document.getChar(p);
 
 381                         if (ch != '=' && ch != ']')
 
 387                         p = firstNonWhitespaceBackward(document, p - 1, partitioning, -1);
 
 391                         ch = document.getChar(p);
 
 392                         if (Scanner.isPHPIdentifierPart(ch) || ch == ']' || ch == '[')
 
 395                 } catch (BadLocationException e) {
 
 401          * Computes an insert position for an opening brace if <code>offset</code>
 
 402          * maps to a position in <code>document</code> involving a keyword taking
 
 403          * a block after it. These are: <code>try</code>, <code>do</code>,
 
 404          * <code>synchronized</code>, <code>static</code>,
 
 405          * <code>finally</code>, or <code>else</code>.
 
 408          *            the document being modified
 
 410          *            the current line under investigation
 
 412          *            the offset of the caret position, relative to the line start.
 
 413          * @return an insert position relative to the line start if
 
 414          *         <code>line</code> contains one of the above keywords at or
 
 415          *         before <code>offset</code>, -1 otherwise
 
 417         private static int computeAfterTryDoElse(IDocument doc,
 
 418                         ITextSelection line, int offset) {
 
 419                 // search backward while WS, find 'try', 'do', 'else' in default
 
 421                 int p = offset + line.getOffset();
 
 422                 p = firstWhitespaceToRight(doc, p);
 
 427                 if (looksLike(doc, p, "try") //$NON-NLS-1$
 
 428                                 || looksLike(doc, p, "do") //$NON-NLS-1$
 
 429                                 || looksLike(doc, p, "synchronized") //$NON-NLS-1$
 
 430                                 || looksLike(doc, p, "static") //$NON-NLS-1$
 
 431                                 || looksLike(doc, p, "finally") //$NON-NLS-1$
 
 432                                 || looksLike(doc, p, "else")) //$NON-NLS-1$
 
 433                         return p + 1 - line.getOffset();
 
 439          * Computes an insert position for an opening brace if <code>offset</code>
 
 440          * maps to a position in <code>document</code> with a expression in
 
 441          * parenthesis that will take a block after the closing parenthesis.
 
 444          *            the document being modified
 
 446          *            the current line under investigation
 
 448          *            the offset of the caret position, relative to the line start.
 
 449          * @param partitioning
 
 450          *            the document partitioning
 
 451          * @return an insert position relative to the line start if
 
 452          *         <code>line</code> contains a parenthesized expression that can
 
 453          *         be followed by a block, -1 otherwise
 
 455         private static int computeAfterParenthesis(IDocument document,
 
 456                         ITextSelection line, int offset, String partitioning) {
 
 457                 // find the opening parenthesis for every closing parenthesis on the
 
 458                 // current line after offset
 
 459                 // return the position behind the closing parenthesis if it looks like a
 
 460                 // method declaration
 
 461                 // or an expression for an if, while, for, catch statement
 
 462                 int pos = offset + line.getOffset();
 
 463                 int length = line.getOffset() + line.getLength();
 
 464                 int scanTo = scanForward(document, pos, partitioning, length, '}');
 
 468                 int closingParen = findClosingParenToLeft(document, pos, partitioning) - 1;
 
 471                         int startScan = closingParen + 1;
 
 472                         closingParen = scanForward(document, startScan, partitioning,
 
 474                         if (closingParen == -1)
 
 477                         int openingParen = findOpeningParenMatch(document, closingParen,
 
 480                         // no way an expression at the beginning of the document can mean
 
 482                         if (openingParen < 1)
 
 485                         // only select insert positions for parenthesis currently embracing
 
 487                         if (openingParen > pos)
 
 490                         if (looksLikeAnonymousClassDef(document, openingParen - 1,
 
 492                                 return closingParen + 1 - line.getOffset();
 
 494                         if (looksLikeIfWhileForCatch(document, openingParen - 1,
 
 496                                 return closingParen + 1 - line.getOffset();
 
 498                         if (looksLikeMethodDecl(document, openingParen - 1, partitioning))
 
 499                                 return closingParen + 1 - line.getOffset();
 
 507          * Finds a closing parenthesis to the left of <code>position</code> in
 
 508          * document, where that parenthesis is only separated by whitespace from
 
 509          * <code>position</code>. If no such parenthesis can be found,
 
 510          * <code>position</code> is returned.
 
 513          *            the document being modified
 
 515          *            the first character position in <code>document</code> to be
 
 517          * @param partitioning
 
 518          *            the document partitioning
 
 519          * @return the position of a closing parenthesis left to
 
 520          *         <code>position</code> separated only by whitespace, or
 
 521          *         <code>position</code> if no parenthesis can be found
 
 523         private static int findClosingParenToLeft(IDocument document, int position,
 
 524                         String partitioning) {
 
 525                 final char CLOSING_PAREN = ')';
 
 530                         int nonWS = firstNonWhitespaceBackward(document, position - 1,
 
 532                         if (nonWS != -1 && document.getChar(nonWS) == CLOSING_PAREN)
 
 534                 } catch (BadLocationException e1) {
 
 540          * Finds the first whitespace character position to the right of (and
 
 541          * including) <code>position</code>.
 
 544          *            the document being modified
 
 546          *            the first character position in <code>document</code> to be
 
 548          * @return the position of a whitespace character greater or equal than
 
 549          *         <code>position</code> separated only by whitespace, or -1 if
 
 552         private static int firstWhitespaceToRight(IDocument document, int position) {
 
 553                 int length = document.getLength();
 
 554                 Assert.isTrue(position >= 0);
 
 555                 Assert.isTrue(position <= length);
 
 558                         while (position < length) {
 
 559                                 char ch = document.getChar(position);
 
 560                                 if (Character.isWhitespace(ch))
 
 565                 } catch (BadLocationException e) {
 
 571          * Finds the highest position in <code>document</code> such that the
 
 572          * position is <= <code>position</code> and > <code>bound</code>
 
 573          * and <code>Character.isWhitespace(document.getChar(pos))</code>
 
 574          * evaluates to <code>false</code> and the position is in the default
 
 578          *            the document being modified
 
 580          *            the first character position in <code>document</code> to be
 
 582          * @param partitioning
 
 583          *            the document partitioning
 
 585          *            the first position in <code>document</code> to not consider
 
 586          *            any more, with <code>bound</code> > <code>position</code>
 
 587          * @return the highest position of one element in <code>chars</code> in [<code>position</code>,
 
 588          *         <code>scanTo</code>) that resides in a Java partition, or
 
 589          *         <code>-1</code> if none can be found
 
 591         private static int firstNonWhitespaceBackward(IDocument document,
 
 592                         int position, String partitioning, int bound) {
 
 593                 Assert.isTrue(position < document.getLength());
 
 594                 Assert.isTrue(bound >= -1);
 
 597                         while (position > bound) {
 
 598                                 char ch = document.getChar(position);
 
 599                                 if (!Character.isWhitespace(ch)
 
 600                                                 && isDefaultPartition(document, position, partitioning))
 
 604                 } catch (BadLocationException e) {
 
 610          * Finds the smallest position in <code>document</code> such that the
 
 611          * position is >= <code>position</code> and < <code>bound</code>
 
 612          * and <code>Character.isWhitespace(document.getChar(pos))</code>
 
 613          * evaluates to <code>false</code> and the position is in the default
 
 617          *            the document being modified
 
 619          *            the first character position in <code>document</code> to be
 
 621          * @param partitioning
 
 622          *            the document partitioning
 
 624          *            the first position in <code>document</code> to not consider
 
 625          *            any more, with <code>bound</code> > <code>position</code>
 
 626          * @return the smallest position of one element in <code>chars</code> in [<code>position</code>,
 
 627          *         <code>scanTo</code>) that resides in a Java partition, or
 
 628          *         <code>-1</code> if none can be found
 
 630         private static int firstNonWhitespaceForward(IDocument document,
 
 631                         int position, String partitioning, int bound) {
 
 632                 Assert.isTrue(position >= 0);
 
 633                 Assert.isTrue(bound <= document.getLength());
 
 636                         while (position < bound) {
 
 637                                 char ch = document.getChar(position);
 
 638                                 if (!Character.isWhitespace(ch)
 
 639                                                 && isDefaultPartition(document, position, partitioning))
 
 643                 } catch (BadLocationException e) {
 
 649          * Finds the highest position in <code>document</code> such that the
 
 650          * position is <= <code>position</code> and > <code>bound</code>
 
 651          * and <code>document.getChar(position) == ch</code> evaluates to
 
 652          * <code>true</code> for at least one ch in <code>chars</code> and the
 
 653          * position is in the default partition.
 
 656          *            the document being modified
 
 658          *            the first character position in <code>document</code> to be
 
 660          * @param partitioning
 
 661          *            the document partitioning
 
 663          *            the first position in <code>document</code> to not consider
 
 664          *            any more, with <code>scanTo</code> >
 
 665          *            <code>position</code>
 
 667          *            an array of <code>char</code> to search for
 
 668          * @return the highest position of one element in <code>chars</code> in (<code>bound</code>,
 
 669          *         <code>position</code>] that resides in a Java partition, or
 
 670          *         <code>-1</code> if none can be found
 
 672         private static int scanBackward(IDocument document, int position,
 
 673                         String partitioning, int bound, char[] chars) {
 
 674                 Assert.isTrue(bound >= -1);
 
 675                 Assert.isTrue(position < document.getLength());
 
 680                         while (position > bound) {
 
 682                                 if (Arrays.binarySearch(chars, document.getChar(position)) >= 0
 
 683                                                 && isDefaultPartition(document, position, partitioning))
 
 688                 } catch (BadLocationException e) {
 
 694         // * Finds the highest position in <code>document</code> such that the
 
 695         // position is <= <code>position</code>
 
 696         // * and > <code>bound</code> and <code>document.getChar(position) ==
 
 697         // ch</code> evaluates to <code>true</code>
 
 698         // * and the position is in the default partition.
 
 700         // * @param document the document being modified
 
 701         // * @param position the first character position in <code>document</code>
 
 703         // * @param bound the first position in <code>document</code> to not
 
 704         // consider any more, with <code>scanTo</code> > <code>position</code>
 
 705         // * @param chars an array of <code>char</code> to search for
 
 706         // * @return the highest position of one element in <code>chars</code> in
 
 707         // [<code>position</code>, <code>scanTo</code>) that resides in a Java
 
 708         // partition, or <code>-1</code> if none can be found
 
 710         // private static int scanBackward(IDocument document, int position, int
 
 712         // return scanBackward(document, position, bound, new char[] {ch});
 
 716          * Finds the lowest position in <code>document</code> such that the
 
 717          * position is >= <code>position</code> and < <code>bound</code>
 
 718          * and <code>document.getChar(position) == ch</code> evaluates to
 
 719          * <code>true</code> for at least one ch in <code>chars</code> and the
 
 720          * position is in the default partition.
 
 723          *            the document being modified
 
 725          *            the first character position in <code>document</code> to be
 
 727          * @param partitioning
 
 728          *            the document partitioning
 
 730          *            the first position in <code>document</code> to not consider
 
 731          *            any more, with <code>scanTo</code> >
 
 732          *            <code>position</code>
 
 734          *            an array of <code>char</code> to search for
 
 735          * @return the lowest position of one element in <code>chars</code> in [<code>position</code>,
 
 736          *         <code>bound</code>) that resides in a Java partition, or
 
 737          *         <code>-1</code> if none can be found
 
 739         private static int scanForward(IDocument document, int position,
 
 740                         String partitioning, int bound, char[] chars) {
 
 741                 Assert.isTrue(position >= 0);
 
 742                 Assert.isTrue(bound <= document.getLength());
 
 747                         while (position < bound) {
 
 749                                 if (Arrays.binarySearch(chars, document.getChar(position)) >= 0
 
 750                                                 && isDefaultPartition(document, position, partitioning))
 
 755                 } catch (BadLocationException e) {
 
 761          * Finds the lowest position in <code>document</code> such that the
 
 762          * position is >= <code>position</code> and < <code>bound</code>
 
 763          * and <code>document.getChar(position) == ch</code> evaluates to
 
 764          * <code>true</code> and the position is in the default partition.
 
 767          *            the document being modified
 
 769          *            the first character position in <code>document</code> to be
 
 771          * @param partitioning
 
 772          *            the document partitioning
 
 774          *            the first position in <code>document</code> to not consider
 
 775          *            any more, with <code>scanTo</code> >
 
 776          *            <code>position</code>
 
 778          *            an array of <code>char</code> to search for
 
 779          * @return the lowest position of one element in <code>chars</code> in [<code>position</code>,
 
 780          *         <code>bound</code>) that resides in a Java partition, or
 
 781          *         <code>-1</code> if none can be found
 
 783         private static int scanForward(IDocument document, int position,
 
 784                         String partitioning, int bound, char ch) {
 
 785                 return scanForward(document, position, partitioning, bound,
 
 790          * Checks whether the content of <code>document</code> in the range (<code>offset</code>,
 
 791          * <code>length</code>) contains the <code>new</code> keyword.
 
 794          *            the document being modified
 
 796          *            the first character position in <code>document</code> to be
 
 799          *            the length of the character range to be considered
 
 800          * @param partitioning
 
 801          *            the document partitioning
 
 802          * @return <code>true</code> if the specified character range contains a
 
 803          *         <code>new</code> keyword, <code>false</code> otherwise.
 
 805         private static boolean isNewMatch(IDocument document, int offset,
 
 806                         int length, String partitioning) {
 
 807                 Assert.isTrue(length >= 0);
 
 808                 Assert.isTrue(offset >= 0);
 
 809                 Assert.isTrue(offset + length < document.getLength() + 1);
 
 812                         String text = document.get(offset, length);
 
 813                         int pos = text.indexOf("new"); //$NON-NLS-1$
 
 816                                         && !isDefaultPartition(document, pos + offset, partitioning))
 
 817                                 pos = text.indexOf("new", pos + 2); //$NON-NLS-1$
 
 822                         if (pos != 0 && Scanner.isPHPIdentifierPart(text.charAt(pos - 1)))
 
 826                                         && Scanner.isPHPIdentifierPart(text.charAt(pos + 3)))
 
 831                 } catch (BadLocationException e) {
 
 837          * Checks whether the content of <code>document</code> at
 
 838          * <code>position</code> looks like an anonymous class definition.
 
 839          * <code>position</code> must be to the left of the opening parenthesis of
 
 840          * the definition's parameter list.
 
 843          *            the document being modified
 
 845          *            the first character position in <code>document</code> to be
 
 847          * @param partitioning
 
 848          *            the document partitioning
 
 849          * @return <code>true</code> if the content of <code>document</code>
 
 850          *         looks like an anonymous class definition, <code>false</code>
 
 853         private static boolean looksLikeAnonymousClassDef(IDocument document,
 
 854                         int position, String partitioning) {
 
 855                 int previousCommaOrParen = scanBackward(document, position - 1,
 
 856                                 partitioning, -1, new char[] { ',', '(' });
 
 857                 if (previousCommaOrParen == -1 || position < previousCommaOrParen + 5) // 2
 
 865                 if (isNewMatch(document, previousCommaOrParen + 1, position
 
 866                                 - previousCommaOrParen - 2, partitioning))
 
 873          * Checks whether <code>position</code> resides in a default (Java)
 
 874          * partition of <code>document</code>.
 
 877          *            the document being modified
 
 879          *            the position to be checked
 
 880          * @param partitioning
 
 881          *            the document partitioning
 
 882          * @return <code>true</code> if <code>position</code> is in the default
 
 883          *         partition of <code>document</code>, <code>false</code>
 
 886         private static boolean isDefaultPartition(IDocument document, int position,
 
 887                         String partitioning) {
 
 888                 Assert.isTrue(position >= 0);
 
 889                 Assert.isTrue(position <= document.getLength());
 
 892                         // don't use getPartition2 since we're interested in the scanned
 
 893                         // character's partition
 
 894                         ITypedRegion region = TextUtilities.getPartition(document,
 
 895                                         partitioning, position, false);
 
 896                         return region.getType().equals(IDocument.DEFAULT_CONTENT_TYPE);
 
 898                 } catch (BadLocationException e) {
 
 905          * Finds the position of the parenthesis matching the closing parenthesis at
 
 906          * <code>position</code>.
 
 909          *            the document being modified
 
 911          *            the position in <code>document</code> of a closing
 
 913          * @param partitioning
 
 914          *            the document partitioning
 
 915          * @return the position in <code>document</code> of the matching
 
 916          *         parenthesis, or -1 if none can be found
 
 918         private static int findOpeningParenMatch(IDocument document, int position,
 
 919                         String partitioning) {
 
 920                 final char CLOSING_PAREN = ')';
 
 921                 final char OPENING_PAREN = '(';
 
 923                 Assert.isTrue(position < document.getLength());
 
 924                 Assert.isTrue(position >= 0);
 
 925                 Assert.isTrue(isDefaultPartition(document, position, partitioning));
 
 929                         Assert.isTrue(document.getChar(position) == CLOSING_PAREN);
 
 933                                 position = scanBackward(document, position - 1, partitioning,
 
 934                                                 -1, new char[] { CLOSING_PAREN, OPENING_PAREN });
 
 938                                 if (document.getChar(position) == CLOSING_PAREN)
 
 947                 } catch (BadLocationException e) {
 
 953          * Checks whether, to the left of <code>position</code> and separated only
 
 954          * by whitespace, <code>document</code> contains a keyword taking a
 
 955          * parameter list and a block after it. These are: <code>if</code>,
 
 956          * <code>while</code>, <code>catch</code>, <code>for</code>,
 
 957          * <code>synchronized</code>, <code>switch</code>.
 
 960          *            the document being modified
 
 962          *            the first character position in <code>document</code> to be
 
 964          * @param partitioning
 
 965          *            the document partitioning
 
 966          * @return <code>true</code> if <code>document</code> contains any of
 
 967          *         the above keywords to the left of <code>position</code>,
 
 968          *         <code>false</code> otherwise
 
 970         private static boolean looksLikeIfWhileForCatch(IDocument document,
 
 971                         int position, String partitioning) {
 
 972                 position = firstNonWhitespaceBackward(document, position, partitioning,
 
 977                 return looksLike(document, position, "if") //$NON-NLS-1$
 
 978                                 || looksLike(document, position, "while") //$NON-NLS-1$
 
 979                                 || looksLike(document, position, "catch") //$NON-NLS-1$
 
 980                                 || looksLike(document, position, "synchronized") //$NON-NLS-1$
 
 981                                 || looksLike(document, position, "switch") //$NON-NLS-1$
 
 982                                 || looksLike(document, position, "for"); //$NON-NLS-1$
 
 986          * Checks whether code>document</code> contains the <code>String</code> <code>like</code>
 
 987          * such that its last character is at <code>position</code>. If <code>like</code>
 
 988          * starts with a identifier part (as determined by
 
 989          * {@link Scanner#isPHPIdentifierPart(char)}), it is also made sure that
 
 990          * <code>like</code> is preceded by some non-identifier character or
 
 991          * stands at the document start.
 
 994          *            the document being modified
 
 996          *            the first character position in <code>document</code> to be
 
 999          *            the <code>String</code> to look for.
 
1000          * @return <code>true</code> if <code>document</code> contains <code>like</code>
 
1001          *         such that it ends at <code>position</code>, <code>false</code>
 
1004         private static boolean looksLike(IDocument document, int position,
 
1006                 int length = like.length();
 
1007                 if (position < length - 1)
 
1011                         if (!like.equals(document.get(position - length + 1, length)))
 
1014                         if (position >= length
 
1015                                         && Scanner.isPHPIdentifierPart(like.charAt(0))
 
1016                                         && Scanner.isPHPIdentifierPart(document.getChar(position
 
1020                 } catch (BadLocationException e) {
 
1028          * Checks whether the content of <code>document</code> at
 
1029          * <code>position</code> looks like a method declaration header (i.e. only
 
1030          * the return type and method name). <code>position</code> must be just
 
1031          * left of the opening parenthesis of the parameter list.
 
1034          *            the document being modified
 
1036          *            the first character position in <code>document</code> to be
 
1038          * @param partitioning
 
1039          *            the document partitioning
 
1040          * @return <code>true</code> if the content of <code>document</code>
 
1041          *         looks like a method definition, <code>false</code> otherwise
 
1043         private static boolean looksLikeMethodDecl(IDocument document,
 
1044                         int position, String partitioning) {
 
1047                 position = eatIdentToLeft(document, position, partitioning);
 
1051                 position = eatBrackets(document, position - 1, partitioning);
 
1055                 position = eatIdentToLeft(document, position - 1, partitioning);
 
1057                 return position != -1;
 
1061          * From <code>position</code> to the left, eats any whitespace and then a
 
1062          * pair of brackets as used to declare an array return type like
 
1066          * </pre>. The return value is either the position of the opening bracket
 
1067          * or <code>position</code> if no pair of brackets can be parsed.
 
1070          *            the document being modified
 
1072          *            the first character position in <code>document</code> to be
 
1074          * @param partitioning
 
1075          *            the document partitioning
 
1076          * @return the smallest character position of bracket pair or
 
1077          *         <code>position</code>
 
1079         private static int eatBrackets(IDocument document, int position,
 
1080                         String partitioning) {
 
1081                 // accept array return type
 
1082                 int pos = firstNonWhitespaceBackward(document, position, partitioning,
 
1085                         if (pos > 1 && document.getChar(pos) == ']') {
 
1086                                 pos = firstNonWhitespaceBackward(document, pos - 1,
 
1088                                 if (pos > 0 && document.getChar(pos) == '[')
 
1091                 } catch (BadLocationException e) {
 
1098          * From <code>position</code> to the left, eats any whitespace and the
 
1099          * first identifier, returning the position of the first identifier
 
1100          * character (in normal read order).
 
1102          * When called on a document with content <code>" some string  "</code> and
 
1103          * positionition 13, the return value will be 6 (the first letter in
 
1104          * <code>string</code>).
 
1108          *            the document being modified
 
1110          *            the first character position in <code>document</code> to be
 
1112          * @param partitioning
 
1113          *            the document partitioning
 
1114          * @return the smallest character position of an identifier or -1 if none
 
1115          *         can be found; always <= <code>position</code>
 
1117         private static int eatIdentToLeft(IDocument document, int position,
 
1118                         String partitioning) {
 
1121                 Assert.isTrue(position < document.getLength());
 
1123                 int p = firstNonWhitespaceBackward(document, position, partitioning, -1);
 
1130                                 char ch = document.getChar(p);
 
1131                                 if (Scanner.isPHPIdentifierPart(ch)) {
 
1136                                 // length must be > 0
 
1137                                 if (Character.isWhitespace(ch) && p != position)
 
1144                         // start of document reached
 
1147                 } catch (BadLocationException e) {
 
1153          * Returns a position in the first java partition after the last non-empty
 
1154          * and non-comment partition. There is no non-whitespace from the returned
 
1155          * position to the end of the partition it is contained in.
 
1158          *            the document being modified
 
1160          *            the line under investigation
 
1162          *            the caret offset into <code>line</code>
 
1163          * @param partitioning
 
1164          *            the document partitioning
 
1165          * @return the position of the next Java partition, or the end of
 
1168         private static int nextPartitionOrLineEnd(IDocument document,
 
1169                         ITextSelection line, int offset, String partitioning) {
 
1170                 // run relative to document
 
1171                 final int docOffset = offset + line.getOffset();
 
1172                 final int eol = line.getOffset() + line.getLength();
 
1173                 int nextPartitionPos = eol; // init with line end
 
1174                 int validPosition = docOffset;
 
1177                         ITypedRegion partition = TextUtilities.getPartition(document,
 
1178                                         partitioning, nextPartitionPos, true);
 
1179                         validPosition = getValidPositionForPartition(document, partition,
 
1181                         while (validPosition == -1) {
 
1182                                 nextPartitionPos = partition.getOffset() - 1;
 
1183                                 if (nextPartitionPos < docOffset) {
 
1184                                         validPosition = docOffset;
 
1187                                 partition = TextUtilities.getPartition(document, partitioning,
 
1188                                                 nextPartitionPos, false);
 
1189                                 validPosition = getValidPositionForPartition(document,
 
1192                 } catch (BadLocationException e) {
 
1195                 validPosition = Math.max(validPosition, docOffset);
 
1196                 // make relative to line
 
1197                 validPosition -= line.getOffset();
 
1198                 return validPosition;
 
1202          * Returns a valid insert location (except for whitespace) in
 
1203          * <code>partition</code> or -1 if there is no valid insert location. An
 
1204          * valid insert location is right after any java string or character
 
1205          * partition, or at the end of a java default partition, but never behind
 
1206          * <code>maxOffset</code>. Comment partitions or empty java partitions do
 
1207          * never yield valid insert positions.
 
1210          *            the document being modified
 
1212          *            the current partition
 
1214          *            the maximum offset to consider
 
1215          * @return a valid insert location in <code>partition</code>, or -1 if
 
1216          *         there is no valid insert location
 
1218         private static int getValidPositionForPartition(IDocument doc,
 
1219                         ITypedRegion partition, int maxOffset) {
 
1220                 final int INVALID = -1;
 
1222                 if (IPHPPartitions.PHP_PHPDOC_COMMENT.equals(partition.getType()))
 
1224                 if (IPHPPartitions.PHP_MULTILINE_COMMENT.equals(partition.getType()))
 
1226                 if (IPHPPartitions.PHP_SINGLELINE_COMMENT.equals(partition.getType()))
 
1229                 int endOffset = Math.min(maxOffset, partition.getOffset()
 
1230                                 + partition.getLength());
 
1232                 // if (IPHPPartitions.JAVA_CHARACTER.equals(partition.getType()))
 
1233                 // return endOffset;
 
1234                 if (IPHPPartitions.PHP_STRING_DQ.equals(partition.getType()))
 
1236                 if (IPHPPartitions.PHP_STRING_SQ.equals(partition.getType()))
 
1238                 if (IPHPPartitions.PHP_STRING_HEREDOC.equals(partition.getType()))
 
1240                 if (IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())) {
 
1242                                 if (doc.get(partition.getOffset(),
 
1243                                                 endOffset - partition.getOffset()).trim().length() == 0)
 
1247                         } catch (BadLocationException e) {
 
1251                 // default: we don't know anything about the partition - assume valid
 
1256          * Determines whether the current line contains a for statement. Algorithm:
 
1257          * any "for" word in the line is a positive, "for" contained in a string
 
1258          * literal will produce a false positive.
 
1261          *            the line where the change is being made
 
1263          *            the position of the caret
 
1264          * @return <code>true</code> if <code>line</code> contains
 
1265          *         <code>for</code>, <code>false</code> otherwise
 
1267         private static boolean isForStatement(String line, int offset) {
 
1268                 /* searching for (^|\s)for(\s|$) */
 
1269                 int forPos = line.indexOf("for"); //$NON-NLS-1$
 
1271                         if ((forPos == 0 || !Scanner.isPHPIdentifierPart(line
 
1272                                         .charAt(forPos - 1)))
 
1273                                         && (line.length() == forPos + 3 || !Scanner
 
1274                                                         .isPHPIdentifierPart(line.charAt(forPos + 3))))
 
1281          * Returns the position in <code>text</code> after which there comes only
 
1282          * whitespace, up to <code>offset</code>.
 
1285          *            the text being searched
 
1287          *            the maximum offset to search for
 
1288          * @return the smallest value <code>v</code> such that
 
1289          *         <code>text.substring(v, offset).trim() == 0</code>
 
1291         private static int startOfWhitespaceBeforeOffset(String text, int offset) {
 
1292                 int i = Math.min(offset, text.length());
 
1293                 for (; i >= 1; i--) {
 
1294                         if (!Character.isWhitespace(text.charAt(i - 1)))