Refactory UI plugin.
[phpeclipse.git] / net.sourceforge.phpeclipse.ui / src / net / sourceforge / phpdt / internal / ui / text / SmartSemicolonAutoEditStrategy.java
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
7  *
8  * Contributors:
9  *     IBM Corporation - initial API and implementation
10  *******************************************************************************/
11 package net.sourceforge.phpdt.internal.ui.text;
12
13 import java.util.Arrays;
14
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;
22
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;
42
43 /**
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.
47  * 
48  * <p>
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.
53  * </p>
54  * 
55  * @see org.eclipse.jface.text.DocumentCommand
56  * @since 3.0
57  */
58 public class SmartSemicolonAutoEditStrategy implements IAutoEditStrategy {
59
60         /** String representation of a semicolon. */
61         private static final String SEMICOLON = ";"; //$NON-NLS-1$
62
63         /** Char representation of a semicolon. */
64         private static final char SEMICHAR = ';';
65
66         /** String represenattion of a opening brace. */
67         private static final String BRACE = "{"; //$NON-NLS-1$
68
69         /** Char representation of a opening brace */
70         private static final char BRACECHAR = '{';
71
72         private char fCharacter;
73
74         private String fPartitioning;
75
76         /**
77          * Creates a new SmartSemicolonAutoEditStrategy.
78          * 
79          * @param partitioning
80          *            the document partitioning
81          */
82         public SmartSemicolonAutoEditStrategy(String partitioning) {
83                 fPartitioning = partitioning;
84         }
85
86         /*
87          * @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument,
88          *      org.eclipse.jface.text.DocumentCommand)
89          */
90         public void customizeDocumentCommand(IDocument document,
91                         DocumentCommand command) {
92                 // 0: early pruning
93                 // also customize if <code>doit</code> is false (so it works in code
94                 // completion situations)
95                 // if (!command.doit)
96                 // return;
97
98                 if (command.text == null)
99                         return;
100
101                 if (command.text.equals(SEMICOLON))
102                         fCharacter = SEMICHAR;
103                 else if (command.text.equals(BRACE))
104                         fCharacter = BRACECHAR;
105                 else
106                         return;
107
108                 IPreferenceStore store = WebUI.getDefault()
109                                 .getPreferenceStore();
110                 if (fCharacter == SEMICHAR
111                                 && !store
112                                                 .getBoolean(PreferenceConstants.EDITOR_SMART_SEMICOLON))
113                         return;
114                 if (fCharacter == BRACECHAR
115                                 && !store
116                                                 .getBoolean(PreferenceConstants.EDITOR_SMART_OPENING_BRACE))
117                         return;
118
119                 IWorkbenchPage page = WebUI.getActivePage();
120                 if (page == null)
121                         return;
122                 IEditorPart part = page.getActiveEditor();
123                 if (!(part instanceof PHPUnitEditor))
124                         return;
125                 PHPUnitEditor editor = (PHPUnitEditor) part;
126                 if (editor.getInsertMode() != ITextEditorExtension3.SMART_INSERT
127                                 || !editor.isEditable())
128                         return;
129                 ITextEditorExtension2 extension = (ITextEditorExtension2) editor
130                                 .getAdapter(ITextEditorExtension2.class);
131                 if (extension != null && !extension.validateEditorInputState())
132                         return;
133                 if (isMultilineSelection(document, command))
134                         return;
135
136                 // 1: find concerned line / position in java code, location in statement
137                 int pos = command.offset;
138                 ITextSelection line;
139                 try {
140                         IRegion l = document.getLineInformationOfOffset(pos);
141                         line = new TextSelection(document, l.getOffset(), l.getLength());
142                 } catch (BadLocationException e) {
143                         return;
144                 }
145
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();
151
152                 // never position before the current position!
153                 if (position < pos)
154                         return;
155
156                 // never double already existing content
157                 if (alreadyPresent(document, fCharacter, position))
158                         return;
159
160                 // don't do special processing if what we do is actually the normal
161                 // behaviour
162                 String insertion = adjustSpacing(document, position, fCharacter);
163                 if (command.offset == position && insertion.equals(command.text))
164                         return;
165
166                 try {
167
168                         final SmartBackspaceManager manager = (SmartBackspaceManager) editor
169                                         .getAdapter(SmartBackspaceManager.class);
170                         if (manager != null
171                                         && WebUI.getDefault().getPreferenceStore()
172                                                         .getBoolean(
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);
179
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);
187                         }
188
189                         // 3: modify command
190                         command.offset = position;
191                         command.length = 0;
192                         command.caretOffset = position;
193                         command.text = insertion;
194                         command.doit = true;
195                         command.owner = null;
196                 } catch (MalformedTreeException e) {
197                         WebUI.log(e);
198                 } catch (BadLocationException e) {
199                         WebUI.log(e);
200                 }
201
202         }
203
204         /**
205          * Returns <code>true</code> if the document command is applied on a multi
206          * line selection, <code>false</code> otherwise.
207          * 
208          * @param document
209          *            the document
210          * @param command
211          *            the command
212          * @return <code>true</code> if <code>command</code> is a multiline
213          *         command
214          */
215         private boolean isMultilineSelection(IDocument document,
216                         DocumentCommand command) {
217                 try {
218                         return document.getNumberOfLines(command.offset, command.length) > 1;
219                 } catch (BadLocationException e) {
220                         // ignore
221                         return false;
222                 }
223         }
224
225         /**
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>.
228          * 
229          * @param document
230          *            the document we are working on
231          * @param position
232          *            the insert position of <code>character</code>
233          * @param character
234          *            the character to be inserted
235          * @return a <code>String</code> consisting of <code>character</code>
236          *         plus any additional spacing
237          */
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 });
252                         }
253                 }
254
255                 return new String(new char[] { character });
256         }
257
258         /**
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>.
262          * 
263          * @param document
264          *            the document we are working on
265          * @param position
266          *            the insert position of <code>ch</code>
267          * @param character
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
271          */
272         private boolean alreadyPresent(IDocument document, char ch, int position) {
273                 int pos = firstNonWhitespaceForward(document, position, fPartitioning,
274                                 document.getLength());
275                 try {
276                         if (pos != -1 && document.getChar(pos) == ch)
277                                 return true;
278                 } catch (BadLocationException e) {
279                 }
280
281                 return false;
282         }
283
284         /**
285          * Computes the next insert position of the given character in the current
286          * line.
287          * 
288          * @param document
289          *            the document we are working on
290          * @param line
291          *            the line where the change is being made
292          * @param offset
293          *            the position of the caret in the line when
294          *            <code>character</code> was typed
295          * @param character
296          *            the character to look for
297          * @param partitioning
298          *            the document partitioning
299          * @return the position where <code>character</code> should be inserted /
300          *         replaced
301          */
302         protected static int computeCharacterPosition(IDocument document,
303                         ITextSelection line, int offset, char character, String partitioning) {
304                 String text = line.getText();
305                 if (text == null)
306                         return 0;
307
308                 int insertPos;
309                 if (character == BRACECHAR) {
310
311                         insertPos = computeArrayInitializationPos(document, line, offset,
312                                         partitioning);
313
314                         if (insertPos == -1) {
315                                 insertPos = computeAfterTryDoElse(document, line, offset);
316                         }
317
318                         if (insertPos == -1) {
319                                 insertPos = computeAfterParenthesis(document, line, offset,
320                                                 partitioning);
321                         }
322
323                 } else if (character == SEMICHAR) {
324
325                         if (isForStatement(text, offset)) {
326                                 insertPos = -1; // don't do anything in for statements, as semis
327                                                                 // are vital part of these
328                         } else {
329                                 int nextPartitionPos = nextPartitionOrLineEnd(document, line,
330                                                 offset, partitioning);
331                                 insertPos = startOfWhitespaceBeforeOffset(text,
332                                                 nextPartitionPos);
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;
337                         }
338
339                 } else {
340                         Assert.isTrue(false);
341                         return -1;
342                 }
343
344                 return insertPos;
345         }
346
347         /**
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.
351          * 
352          * @param document
353          *            the document being modified
354          * @param line
355          *            the current line under investigation
356          * @param offset
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
363          */
364         private static int computeArrayInitializationPos(IDocument document,
365                         ITextSelection line, int offset, String partitioning) {
366                 // search backward while WS, find = (not != <= >= ==) in default
367                 // partition
368                 int pos = offset + line.getOffset();
369
370                 if (pos == 0)
371                         return -1;
372
373                 int p = firstNonWhitespaceBackward(document, pos - 1, partitioning, -1);
374
375                 if (p == -1)
376                         return -1;
377
378                 try {
379
380                         char ch = document.getChar(p);
381                         if (ch != '=' && ch != ']')
382                                 return -1;
383
384                         if (p == 0)
385                                 return offset;
386
387                         p = firstNonWhitespaceBackward(document, p - 1, partitioning, -1);
388                         if (p == -1)
389                                 return -1;
390
391                         ch = document.getChar(p);
392                         if (Scanner.isPHPIdentifierPart(ch) || ch == ']' || ch == '[')
393                                 return offset;
394
395                 } catch (BadLocationException e) {
396                 }
397                 return -1;
398         }
399
400         /**
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>.
406          * 
407          * @param document
408          *            the document being modified
409          * @param line
410          *            the current line under investigation
411          * @param offset
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
416          */
417         private static int computeAfterTryDoElse(IDocument doc,
418                         ITextSelection line, int offset) {
419                 // search backward while WS, find 'try', 'do', 'else' in default
420                 // partition
421                 int p = offset + line.getOffset();
422                 p = firstWhitespaceToRight(doc, p);
423                 if (p == -1)
424                         return -1;
425                 p--;
426
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();
434
435                 return -1;
436         }
437
438         /**
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.
442          * 
443          * @param document
444          *            the document being modified
445          * @param line
446          *            the current line under investigation
447          * @param offset
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
454          */
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, '}');
465                 if (scanTo == -1)
466                         scanTo = length;
467
468                 int closingParen = findClosingParenToLeft(document, pos, partitioning) - 1;
469
470                 while (true) {
471                         int startScan = closingParen + 1;
472                         closingParen = scanForward(document, startScan, partitioning,
473                                         scanTo, ')');
474                         if (closingParen == -1)
475                                 break;
476
477                         int openingParen = findOpeningParenMatch(document, closingParen,
478                                         partitioning);
479
480                         // no way an expression at the beginning of the document can mean
481                         // anything
482                         if (openingParen < 1)
483                                 break;
484
485                         // only select insert positions for parenthesis currently embracing
486                         // the caret
487                         if (openingParen > pos)
488                                 continue;
489
490                         if (looksLikeAnonymousClassDef(document, openingParen - 1,
491                                         partitioning))
492                                 return closingParen + 1 - line.getOffset();
493
494                         if (looksLikeIfWhileForCatch(document, openingParen - 1,
495                                         partitioning))
496                                 return closingParen + 1 - line.getOffset();
497
498                         if (looksLikeMethodDecl(document, openingParen - 1, partitioning))
499                                 return closingParen + 1 - line.getOffset();
500
501                 }
502
503                 return -1;
504         }
505
506         /**
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.
511          * 
512          * @param document
513          *            the document being modified
514          * @param position
515          *            the first character position in <code>document</code> to be
516          *            considered
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
522          */
523         private static int findClosingParenToLeft(IDocument document, int position,
524                         String partitioning) {
525                 final char CLOSING_PAREN = ')';
526                 try {
527                         if (position < 1)
528                                 return position;
529
530                         int nonWS = firstNonWhitespaceBackward(document, position - 1,
531                                         partitioning, -1);
532                         if (nonWS != -1 && document.getChar(nonWS) == CLOSING_PAREN)
533                                 return nonWS;
534                 } catch (BadLocationException e1) {
535                 }
536                 return position;
537         }
538
539         /**
540          * Finds the first whitespace character position to the right of (and
541          * including) <code>position</code>.
542          * 
543          * @param document
544          *            the document being modified
545          * @param position
546          *            the first character position in <code>document</code> to be
547          *            considered
548          * @return the position of a whitespace character greater or equal than
549          *         <code>position</code> separated only by whitespace, or -1 if
550          *         none found
551          */
552         private static int firstWhitespaceToRight(IDocument document, int position) {
553                 int length = document.getLength();
554                 Assert.isTrue(position >= 0);
555                 Assert.isTrue(position <= length);
556
557                 try {
558                         while (position < length) {
559                                 char ch = document.getChar(position);
560                                 if (Character.isWhitespace(ch))
561                                         return position;
562                                 position++;
563                         }
564                         return position;
565                 } catch (BadLocationException e) {
566                 }
567                 return -1;
568         }
569
570         /**
571          * Finds the highest position in <code>document</code> such that the
572          * position is &lt;= <code>position</code> and &gt; <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
575          * partition.
576          * 
577          * @param document
578          *            the document being modified
579          * @param position
580          *            the first character position in <code>document</code> to be
581          *            considered
582          * @param partitioning
583          *            the document partitioning
584          * @param bound
585          *            the first position in <code>document</code> to not consider
586          *            any more, with <code>bound</code> &gt; <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
590          */
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);
595
596                 try {
597                         while (position > bound) {
598                                 char ch = document.getChar(position);
599                                 if (!Character.isWhitespace(ch)
600                                                 && isDefaultPartition(document, position, partitioning))
601                                         return position;
602                                 position--;
603                         }
604                 } catch (BadLocationException e) {
605                 }
606                 return -1;
607         }
608
609         /**
610          * Finds the smallest position in <code>document</code> such that the
611          * position is &gt;= <code>position</code> and &lt; <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
614          * partition.
615          * 
616          * @param document
617          *            the document being modified
618          * @param position
619          *            the first character position in <code>document</code> to be
620          *            considered
621          * @param partitioning
622          *            the document partitioning
623          * @param bound
624          *            the first position in <code>document</code> to not consider
625          *            any more, with <code>bound</code> &gt; <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
629          */
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());
634
635                 try {
636                         while (position < bound) {
637                                 char ch = document.getChar(position);
638                                 if (!Character.isWhitespace(ch)
639                                                 && isDefaultPartition(document, position, partitioning))
640                                         return position;
641                                 position++;
642                         }
643                 } catch (BadLocationException e) {
644                 }
645                 return -1;
646         }
647
648         /**
649          * Finds the highest position in <code>document</code> such that the
650          * position is &lt;= <code>position</code> and &gt; <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.
654          * 
655          * @param document
656          *            the document being modified
657          * @param position
658          *            the first character position in <code>document</code> to be
659          *            considered
660          * @param partitioning
661          *            the document partitioning
662          * @param bound
663          *            the first position in <code>document</code> to not consider
664          *            any more, with <code>scanTo</code> &gt;
665          *            <code>position</code>
666          * @param chars
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
671          */
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());
676
677                 Arrays.sort(chars);
678
679                 try {
680                         while (position > bound) {
681
682                                 if (Arrays.binarySearch(chars, document.getChar(position)) >= 0
683                                                 && isDefaultPartition(document, position, partitioning))
684                                         return position;
685
686                                 position--;
687                         }
688                 } catch (BadLocationException e) {
689                 }
690                 return -1;
691         }
692
693         // /**
694         // * Finds the highest position in <code>document</code> such that the
695         // position is &lt;= <code>position</code>
696         // * and &gt; <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.
699         // *
700         // * @param document the document being modified
701         // * @param position the first character position in <code>document</code>
702         // to be considered
703         // * @param bound the first position in <code>document</code> to not
704         // consider any more, with <code>scanTo</code> &gt; <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
709         // */
710         // private static int scanBackward(IDocument document, int position, int
711         // bound, char ch) {
712         // return scanBackward(document, position, bound, new char[] {ch});
713         // }
714         //
715         /**
716          * Finds the lowest position in <code>document</code> such that the
717          * position is &gt;= <code>position</code> and &lt; <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.
721          * 
722          * @param document
723          *            the document being modified
724          * @param position
725          *            the first character position in <code>document</code> to be
726          *            considered
727          * @param partitioning
728          *            the document partitioning
729          * @param bound
730          *            the first position in <code>document</code> to not consider
731          *            any more, with <code>scanTo</code> &gt;
732          *            <code>position</code>
733          * @param chars
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
738          */
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());
743
744                 Arrays.sort(chars);
745
746                 try {
747                         while (position < bound) {
748
749                                 if (Arrays.binarySearch(chars, document.getChar(position)) >= 0
750                                                 && isDefaultPartition(document, position, partitioning))
751                                         return position;
752
753                                 position++;
754                         }
755                 } catch (BadLocationException e) {
756                 }
757                 return -1;
758         }
759
760         /**
761          * Finds the lowest position in <code>document</code> such that the
762          * position is &gt;= <code>position</code> and &lt; <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.
765          * 
766          * @param document
767          *            the document being modified
768          * @param position
769          *            the first character position in <code>document</code> to be
770          *            considered
771          * @param partitioning
772          *            the document partitioning
773          * @param bound
774          *            the first position in <code>document</code> to not consider
775          *            any more, with <code>scanTo</code> &gt;
776          *            <code>position</code>
777          * @param chars
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
782          */
783         private static int scanForward(IDocument document, int position,
784                         String partitioning, int bound, char ch) {
785                 return scanForward(document, position, partitioning, bound,
786                                 new char[] { ch });
787         }
788
789         /**
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.
792          * 
793          * @param document
794          *            the document being modified
795          * @param offset
796          *            the first character position in <code>document</code> to be
797          *            considered
798          * @param length
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.
804          */
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);
810
811                 try {
812                         String text = document.get(offset, length);
813                         int pos = text.indexOf("new"); //$NON-NLS-1$
814
815                         while (pos != -1
816                                         && !isDefaultPartition(document, pos + offset, partitioning))
817                                 pos = text.indexOf("new", pos + 2); //$NON-NLS-1$
818
819                         if (pos < 0)
820                                 return false;
821
822                         if (pos != 0 && Scanner.isPHPIdentifierPart(text.charAt(pos - 1)))
823                                 return false;
824
825                         if (pos + 3 < length
826                                         && Scanner.isPHPIdentifierPart(text.charAt(pos + 3)))
827                                 return false;
828
829                         return true;
830
831                 } catch (BadLocationException e) {
832                 }
833                 return false;
834         }
835
836         /**
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.
841          * 
842          * @param document
843          *            the document being modified
844          * @param position
845          *            the first character position in <code>document</code> to be
846          *            considered
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>
851          *         otherwise
852          */
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
858                                                                                                                                                                 // for
859                                                                                                                                                                 // borders,
860                                                                                                                                                                 // 3
861                                                                                                                                                                 // for
862                                                                                                                                                                 // "new"
863                         return false;
864
865                 if (isNewMatch(document, previousCommaOrParen + 1, position
866                                 - previousCommaOrParen - 2, partitioning))
867                         return true;
868
869                 return false;
870         }
871
872         /**
873          * Checks whether <code>position</code> resides in a default (Java)
874          * partition of <code>document</code>.
875          * 
876          * @param document
877          *            the document being modified
878          * @param position
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>
884          *         otherwise
885          */
886         private static boolean isDefaultPartition(IDocument document, int position,
887                         String partitioning) {
888                 Assert.isTrue(position >= 0);
889                 Assert.isTrue(position <= document.getLength());
890
891                 try {
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);
897
898                 } catch (BadLocationException e) {
899                 }
900
901                 return false;
902         }
903
904         /**
905          * Finds the position of the parenthesis matching the closing parenthesis at
906          * <code>position</code>.
907          * 
908          * @param document
909          *            the document being modified
910          * @param position
911          *            the position in <code>document</code> of a closing
912          *            parenthesis
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
917          */
918         private static int findOpeningParenMatch(IDocument document, int position,
919                         String partitioning) {
920                 final char CLOSING_PAREN = ')';
921                 final char OPENING_PAREN = '(';
922
923                 Assert.isTrue(position < document.getLength());
924                 Assert.isTrue(position >= 0);
925                 Assert.isTrue(isDefaultPartition(document, position, partitioning));
926
927                 try {
928
929                         Assert.isTrue(document.getChar(position) == CLOSING_PAREN);
930
931                         int depth = 1;
932                         while (true) {
933                                 position = scanBackward(document, position - 1, partitioning,
934                                                 -1, new char[] { CLOSING_PAREN, OPENING_PAREN });
935                                 if (position == -1)
936                                         return -1;
937
938                                 if (document.getChar(position) == CLOSING_PAREN)
939                                         depth++;
940                                 else
941                                         depth--;
942
943                                 if (depth == 0)
944                                         return position;
945                         }
946
947                 } catch (BadLocationException e) {
948                         return -1;
949                 }
950         }
951
952         /**
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>.
958          * 
959          * @param document
960          *            the document being modified
961          * @param position
962          *            the first character position in <code>document</code> to be
963          *            considered
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
969          */
970         private static boolean looksLikeIfWhileForCatch(IDocument document,
971                         int position, String partitioning) {
972                 position = firstNonWhitespaceBackward(document, position, partitioning,
973                                 -1);
974                 if (position == -1)
975                         return false;
976
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$
983         }
984
985         /**
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.
992          * 
993          * @param document
994          *            the document being modified
995          * @param position
996          *            the first character position in <code>document</code> to be
997          *            considered
998          * @param like
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>
1002          *         otherwise
1003          */
1004         private static boolean looksLike(IDocument document, int position,
1005                         String like) {
1006                 int length = like.length();
1007                 if (position < length - 1)
1008                         return false;
1009
1010                 try {
1011                         if (!like.equals(document.get(position - length + 1, length)))
1012                                 return false;
1013
1014                         if (position >= length
1015                                         && Scanner.isPHPIdentifierPart(like.charAt(0))
1016                                         && Scanner.isPHPIdentifierPart(document.getChar(position
1017                                                         - length)))
1018                                 return false;
1019
1020                 } catch (BadLocationException e) {
1021                         return false;
1022                 }
1023
1024                 return true;
1025         }
1026
1027         /**
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.
1032          * 
1033          * @param document
1034          *            the document being modified
1035          * @param position
1036          *            the first character position in <code>document</code> to be
1037          *            considered
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
1042          */
1043         private static boolean looksLikeMethodDecl(IDocument document,
1044                         int position, String partitioning) {
1045
1046                 // method name
1047                 position = eatIdentToLeft(document, position, partitioning);
1048                 if (position < 1)
1049                         return false;
1050
1051                 position = eatBrackets(document, position - 1, partitioning);
1052                 if (position < 1)
1053                         return false;
1054
1055                 position = eatIdentToLeft(document, position - 1, partitioning);
1056
1057                 return position != -1;
1058         }
1059
1060         /**
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
1063          * 
1064          * <pre>
1065          * String [ ]
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.
1068          * 
1069          * @param document
1070          *            the document being modified
1071          * @param position
1072          *            the first character position in <code>document</code> to be
1073          *            considered
1074          * @param partitioning
1075          *            the document partitioning
1076          * @return the smallest character position of bracket pair or
1077          *         <code>position</code>
1078          */
1079         private static int eatBrackets(IDocument document, int position,
1080                         String partitioning) {
1081                 // accept array return type
1082                 int pos = firstNonWhitespaceBackward(document, position, partitioning,
1083                                 -1);
1084                 try {
1085                         if (pos > 1 && document.getChar(pos) == ']') {
1086                                 pos = firstNonWhitespaceBackward(document, pos - 1,
1087                                                 partitioning, -1);
1088                                 if (pos > 0 && document.getChar(pos) == '[')
1089                                         return pos;
1090                         }
1091                 } catch (BadLocationException e) {
1092                         // won't happen
1093                 }
1094                 return position;
1095         }
1096
1097         /**
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).
1101          * <p>
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>).
1105          * </p>
1106          * 
1107          * @param document
1108          *            the document being modified
1109          * @param position
1110          *            the first character position in <code>document</code> to be
1111          *            considered
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 &lt;= <code>position</code>
1116          */
1117         private static int eatIdentToLeft(IDocument document, int position,
1118                         String partitioning) {
1119                 if (position < 0)
1120                         return -1;
1121                 Assert.isTrue(position < document.getLength());
1122
1123                 int p = firstNonWhitespaceBackward(document, position, partitioning, -1);
1124                 if (p == -1)
1125                         return -1;
1126
1127                 try {
1128                         while (p >= 0) {
1129
1130                                 char ch = document.getChar(p);
1131                                 if (Scanner.isPHPIdentifierPart(ch)) {
1132                                         p--;
1133                                         continue;
1134                                 }
1135
1136                                 // length must be > 0
1137                                 if (Character.isWhitespace(ch) && p != position)
1138                                         return p + 1;
1139                                 else
1140                                         return -1;
1141
1142                         }
1143
1144                         // start of document reached
1145                         return 0;
1146
1147                 } catch (BadLocationException e) {
1148                 }
1149                 return -1;
1150         }
1151
1152         /**
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.
1156          * 
1157          * @param document
1158          *            the document being modified
1159          * @param line
1160          *            the line under investigation
1161          * @param offset
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
1166          *         <code>line</code>
1167          */
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;
1175
1176                 try {
1177                         ITypedRegion partition = TextUtilities.getPartition(document,
1178                                         partitioning, nextPartitionPos, true);
1179                         validPosition = getValidPositionForPartition(document, partition,
1180                                         eol);
1181                         while (validPosition == -1) {
1182                                 nextPartitionPos = partition.getOffset() - 1;
1183                                 if (nextPartitionPos < docOffset) {
1184                                         validPosition = docOffset;
1185                                         break;
1186                                 }
1187                                 partition = TextUtilities.getPartition(document, partitioning,
1188                                                 nextPartitionPos, false);
1189                                 validPosition = getValidPositionForPartition(document,
1190                                                 partition, eol);
1191                         }
1192                 } catch (BadLocationException e) {
1193                 }
1194
1195                 validPosition = Math.max(validPosition, docOffset);
1196                 // make relative to line
1197                 validPosition -= line.getOffset();
1198                 return validPosition;
1199         }
1200
1201         /**
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.
1208          * 
1209          * @param doc
1210          *            the document being modified
1211          * @param partition
1212          *            the current partition
1213          * @param maxOffset
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
1217          */
1218         private static int getValidPositionForPartition(IDocument doc,
1219                         ITypedRegion partition, int maxOffset) {
1220                 final int INVALID = -1;
1221
1222                 if (IPHPPartitions.PHP_PHPDOC_COMMENT.equals(partition.getType()))
1223                         return INVALID;
1224                 if (IPHPPartitions.PHP_MULTILINE_COMMENT.equals(partition.getType()))
1225                         return INVALID;
1226                 if (IPHPPartitions.PHP_SINGLELINE_COMMENT.equals(partition.getType()))
1227                         return INVALID;
1228
1229                 int endOffset = Math.min(maxOffset, partition.getOffset()
1230                                 + partition.getLength());
1231
1232                 // if (IPHPPartitions.JAVA_CHARACTER.equals(partition.getType()))
1233                 // return endOffset;
1234                 if (IPHPPartitions.PHP_STRING_DQ.equals(partition.getType()))
1235                         return endOffset;
1236                 if (IPHPPartitions.PHP_STRING_SQ.equals(partition.getType()))
1237                         return endOffset;
1238                 if (IPHPPartitions.PHP_STRING_HEREDOC.equals(partition.getType()))
1239                         return endOffset;
1240                 if (IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())) {
1241                         try {
1242                                 if (doc.get(partition.getOffset(),
1243                                                 endOffset - partition.getOffset()).trim().length() == 0)
1244                                         return INVALID;
1245                                 else
1246                                         return endOffset;
1247                         } catch (BadLocationException e) {
1248                                 return INVALID;
1249                         }
1250                 }
1251                 // default: we don't know anything about the partition - assume valid
1252                 return endOffset;
1253         }
1254
1255         /**
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.
1259          * 
1260          * @param line
1261          *            the line where the change is being made
1262          * @param offset
1263          *            the position of the caret
1264          * @return <code>true</code> if <code>line</code> contains
1265          *         <code>for</code>, <code>false</code> otherwise
1266          */
1267         private static boolean isForStatement(String line, int offset) {
1268                 /* searching for (^|\s)for(\s|$) */
1269                 int forPos = line.indexOf("for"); //$NON-NLS-1$
1270                 if (forPos != -1) {
1271                         if ((forPos == 0 || !Scanner.isPHPIdentifierPart(line
1272                                         .charAt(forPos - 1)))
1273                                         && (line.length() == forPos + 3 || !Scanner
1274                                                         .isPHPIdentifierPart(line.charAt(forPos + 3))))
1275                                 return true;
1276                 }
1277                 return false;
1278         }
1279
1280         /**
1281          * Returns the position in <code>text</code> after which there comes only
1282          * whitespace, up to <code>offset</code>.
1283          * 
1284          * @param text
1285          *            the text being searched
1286          * @param offset
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>
1290          */
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)))
1295                                 break;
1296                 }
1297                 return i;
1298         }
1299 }