1) Fixed issue #872.
[phpeclipse.git] / net.sourceforge.phpeclipse / 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 //incastrix
17 //import net.sourceforge.phpdt.internal.corext.Assert;
18 import org.eclipse.core.runtime.Assert;
19 import net.sourceforge.phpdt.internal.ui.text.SmartBackspaceManager.UndoSpec;
20 import net.sourceforge.phpdt.ui.PreferenceConstants;
21 import net.sourceforge.phpeclipse.PHPeclipsePlugin;
22 import net.sourceforge.phpeclipse.phpeditor.PHPUnitEditor;
23
24 import org.eclipse.jface.preference.IPreferenceStore;
25 import org.eclipse.jface.text.BadLocationException;
26 import org.eclipse.jface.text.DocumentCommand;
27 import org.eclipse.jface.text.IAutoEditStrategy;
28 import org.eclipse.jface.text.IDocument;
29 import org.eclipse.jface.text.IRegion;
30 import org.eclipse.jface.text.ITextSelection;
31 import org.eclipse.jface.text.ITypedRegion;
32 import org.eclipse.jface.text.Region;
33 import org.eclipse.jface.text.TextSelection;
34 import org.eclipse.jface.text.TextUtilities;
35 import org.eclipse.text.edits.DeleteEdit;
36 import org.eclipse.text.edits.MalformedTreeException;
37 import org.eclipse.text.edits.ReplaceEdit;
38 import org.eclipse.text.edits.TextEdit;
39 import org.eclipse.ui.IEditorPart;
40 import org.eclipse.ui.IWorkbenchPage;
41 import org.eclipse.ui.texteditor.ITextEditorExtension2;
42 import org.eclipse.ui.texteditor.ITextEditorExtension3;
43
44 /**
45  * Modifies <code>DocumentCommand</code>s inserting semicolons and opening
46  * braces to place them smartly, i.e. moving them to the end of a line if that
47  * is what the user expects.
48  * 
49  * <p>
50  * In practice, semicolons and braces (and the caret) are moved to the end of
51  * the line if they are typed anywhere except for semicolons in a
52  * <code>for</code> statements definition. If the line contains a semicolon or
53  * brace after the current caret position, the cursor is moved after it.
54  * </p>
55  * 
56  * @see org.eclipse.jface.text.DocumentCommand
57  * @since 3.0
58  */
59 public class SmartSemicolonAutoEditStrategy implements IAutoEditStrategy {
60
61         /** String representation of a semicolon. */
62         private static final String SEMICOLON = ";"; //$NON-NLS-1$
63
64         /** Char representation of a semicolon. */
65         private static final char SEMICHAR = ';';
66
67         /** String representation of a opening brace. */
68         private static final String BRACE = "{"; //$NON-NLS-1$
69
70         /** Char representation of a opening brace */
71         private static final char BRACECHAR = '{';
72
73         private char fCharacter;
74
75         private String fPartitioning;
76
77         /**
78          * Creates a new SmartSemicolonAutoEditStrategy.
79          * 
80          * @param partitioning
81          *            the document partitioning
82          */
83         public SmartSemicolonAutoEditStrategy(String partitioning) {
84                 fPartitioning = partitioning;
85         }
86
87         /*
88          * @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument,
89          *      org.eclipse.jface.text.DocumentCommand)
90          */
91         public void customizeDocumentCommand(IDocument document,
92                         DocumentCommand command) {
93                 // 0: early pruning
94                 // also customize if <code>doit</code> is false (so it works in code
95                 // completion situations)
96                 // if (!command.doit)
97                 // return;
98
99                 if (command.text == null)
100                         return;
101
102                 if (command.text.equals(SEMICOLON))
103                         fCharacter = SEMICHAR;
104                 else if (command.text.equals(BRACE))
105                         fCharacter = BRACECHAR;
106                 else
107                         return;
108
109                 IPreferenceStore store = PHPeclipsePlugin.getDefault()
110                                 .getPreferenceStore();
111                 if (fCharacter == SEMICHAR
112                                 && !store
113                                                 .getBoolean(PreferenceConstants.EDITOR_SMART_SEMICOLON))
114                         return;
115                 if (fCharacter == BRACECHAR
116                                 && !store
117                                                 .getBoolean(PreferenceConstants.EDITOR_SMART_OPENING_BRACE))
118                         return;
119
120                 IWorkbenchPage page = PHPeclipsePlugin.getActivePage();
121                 if (page == null)
122                         return;
123                 IEditorPart part = page.getActiveEditor();
124                 if (!(part instanceof PHPUnitEditor))
125                         return;
126                 PHPUnitEditor editor = (PHPUnitEditor) part;
127                 if (editor.getInsertMode() != ITextEditorExtension3.SMART_INSERT
128                                 || !editor.isEditable())
129                         return;
130                 ITextEditorExtension2 extension = (ITextEditorExtension2) editor
131                                 .getAdapter(ITextEditorExtension2.class);
132                 if (extension != null && !extension.validateEditorInputState())
133                         return;
134                 if (isMultilineSelection(document, command))
135                         return;
136
137                 // 1: find concerned line / position in java code, location in statement
138                 int pos = command.offset;
139                 ITextSelection line;
140                 try {
141                         IRegion l = document.getLineInformationOfOffset(pos);
142                         line = new TextSelection(document, l.getOffset(), l.getLength());
143                 } catch (BadLocationException e) {
144                         return;
145                 }
146
147                 // 2: choose action based on findings (is for-Statement?)
148                 // for now: compute the best position to insert the new character
149                 int positionInLine = computeCharacterPosition(document, line, pos
150                                 - line.getOffset(), fCharacter, fPartitioning);
151                 int position = positionInLine + line.getOffset();
152
153                 // never position before the current position!
154                 if (position < pos)
155                         return;
156
157                 // never double already existing content
158                 if (alreadyPresent(document, fCharacter, position))
159                         return;
160
161                 // don't do special processing if what we do is actually the normal
162                 // behaviour
163                 String insertion = adjustSpacing(document, position, fCharacter);
164                 if (command.offset == position && insertion.equals(command.text))
165                         return;
166
167                 try {
168
169                         final SmartBackspaceManager manager = (SmartBackspaceManager) editor
170                                         .getAdapter(SmartBackspaceManager.class);
171                         if (manager != null
172                                         && PHPeclipsePlugin.getDefault().getPreferenceStore()
173                                                         .getBoolean(
174                                                                         PreferenceConstants.EDITOR_SMART_BACKSPACE)) {
175                                 TextEdit e1 = new ReplaceEdit(command.offset, command.text
176                                                 .length(), document.get(command.offset, command.length));
177                                 UndoSpec s1 = new UndoSpec(command.offset
178                                                 + command.text.length(), new Region(command.offset, 0),
179                                                 new TextEdit[] { e1 }, 0, null);
180
181                                 DeleteEdit smart = new DeleteEdit(position, insertion.length());
182                                 ReplaceEdit raw = new ReplaceEdit(command.offset,
183                                                 command.length, command.text);
184                                 UndoSpec s2 = new UndoSpec(position + insertion.length(),
185                                                 new Region(command.offset + command.text.length(), 0),
186                                                 new TextEdit[] { smart, raw }, 2, s1);
187                                 manager.register(s2);
188                         }
189
190                         // 3: modify command
191                         command.offset = position;
192                         command.length = 0;
193                         command.caretOffset = position;
194                         command.text = insertion;
195                         command.doit = true;
196                         command.owner = null;
197                 } catch (MalformedTreeException e) {
198                         PHPeclipsePlugin.log(e);
199                 } catch (BadLocationException e) {
200                         PHPeclipsePlugin.log(e);
201                 }
202
203         }
204
205         /**
206          * Returns <code>true</code> if the document command is applied on a multi
207          * line selection, <code>false</code> otherwise.
208          * 
209          * @param document
210          *            the document
211          * @param command
212          *            the command
213          * @return <code>true</code> if <code>command</code> is a multiline
214          *         command
215          */
216         private boolean isMultilineSelection(IDocument document,
217                         DocumentCommand command) {
218                 try {
219                         return document.getNumberOfLines(command.offset, command.length) > 1;
220                 } catch (BadLocationException e) {
221                         // ignore
222                         return false;
223                 }
224         }
225
226         /**
227          * Adds a space before a brace if it is inserted after a parenthesis, equal
228          * sign, or one of the keywords <code>try, else, do</code>.
229          * 
230          * @param document
231          *            the document we are working on
232          * @param position
233          *            the insert position of <code>character</code>
234          * @param character
235          *            the character to be inserted
236          * @return a <code>String</code> consisting of <code>character</code>
237          *         plus any additional spacing
238          */
239         private String adjustSpacing(IDocument doc, int position, char character) {
240                 if (character == BRACECHAR) {
241                         if (position > 0 && position <= doc.getLength()) {
242                                 int pos = position - 1;
243                                 if (looksLike(doc, pos, ")") //$NON-NLS-1$
244                                                 || looksLike(doc, pos, "=") //$NON-NLS-1$
245                                                 || looksLike(doc, pos, "]") //$NON-NLS-1$
246                                                 || looksLike(doc, pos, "try") //$NON-NLS-1$
247                                                 || looksLike(doc, pos, "else") //$NON-NLS-1$
248                                                 || looksLike(doc, pos, "synchronized") //$NON-NLS-1$
249                                                 || looksLike(doc, pos, "static") //$NON-NLS-1$
250                                                 || looksLike(doc, pos, "finally") //$NON-NLS-1$
251                                                 || looksLike(doc, pos, "do")) //$NON-NLS-1$
252                                         return new String(new char[] { ' ', character });
253                         }
254                 }
255
256                 return new String(new char[] { character });
257         }
258
259         /**
260          * Checks whether a character to be inserted is already present at the
261          * insert location (perhaps separated by some whitespace from
262          * <code>position</code>.
263          * 
264          * @param document
265          *            the document we are working on
266          * @param position
267          *            the insert position of <code>ch</code>
268          * @param character
269          *            the character to be inserted
270          * @return <code>true</code> if <code>ch</code> is already present at
271          *         <code>location</code>, <code>false</code> otherwise
272          */
273         private boolean alreadyPresent(IDocument document, char ch, int position) {
274                 int pos = firstNonWhitespaceForward(document, position, fPartitioning,
275                                 document.getLength());
276                 try {
277                         if (pos != -1 && document.getChar(pos) == ch)
278                                 return true;
279                 } catch (BadLocationException e) {
280                 }
281
282                 return false;
283         }
284
285         /**
286          * Computes the next insert position of the given character in the current
287          * line.
288          * 
289          * @param document
290          *            the document we are working on
291          * @param line
292          *            the line where the change is being made
293          * @param offset
294          *            the position of the caret in the line when
295          *            <code>character</code> was typed
296          * @param character
297          *            the character to look for
298          * @param partitioning
299          *            the document partitioning
300          * @return the position where <code>character</code> should be inserted /
301          *         replaced
302          */
303         protected static int computeCharacterPosition(IDocument document,
304                         ITextSelection line, int offset, char character, String partitioning) {
305                 String text = line.getText();
306                 if (text == null)
307                         return 0;
308
309                 int insertPos;
310                 if (character == BRACECHAR) {
311
312                         insertPos = computeArrayInitializationPos(document, line, offset,
313                                         partitioning);
314
315                         if (insertPos == -1) {
316                                 insertPos = computeAfterTryDoElse(document, line, offset);
317                         }
318
319                         if (insertPos == -1) {
320                                 insertPos = computeAfterParenthesis(document, line, offset,
321                                                 partitioning);
322                         }
323
324                 } else if (character == SEMICHAR) {
325
326                         if (isForStatement(text, offset)) {
327                                 insertPos = -1; // don't do anything in for statements, as semis
328                                                                 // are vital part of these
329                         } else {
330                                 int nextPartitionPos = nextPartitionOrLineEnd(document, line,
331                                                 offset, partitioning);
332                                 insertPos = startOfWhitespaceBeforeOffset(text,
333                                                 nextPartitionPos);
334                                 // if there is a semi present, return its location as
335                                 // alreadyPresent() will take it out this way.
336                                 if (insertPos > 0 && text.charAt(insertPos - 1) == character)
337                                         insertPos = insertPos - 1;
338                         }
339
340                 } else {
341                         Assert.isTrue(false);
342                         return -1;
343                 }
344
345                 return insertPos;
346         }
347
348         /**
349          * Computes an insert position for an opening brace if <code>offset</code>
350          * maps to a position in <code>document</code> that looks like being the
351          * RHS of an assignment or like an array definition.
352          * 
353          * @param document
354          *            the document being modified
355          * @param line
356          *            the current line under investigation
357          * @param offset
358          *            the offset of the caret position, relative to the line start.
359          * @param partitioning
360          *            the document partitioning
361          * @return an insert position relative to the line start if
362          *         <code>line</code> looks like being an array initialization at
363          *         <code>offset</code>, -1 otherwise
364          */
365         private static int computeArrayInitializationPos(IDocument document,
366                         ITextSelection line, int offset, String partitioning) {
367                 // search backward while WS, find = (not != <= >= ==) in default
368                 // partition
369                 int pos = offset + line.getOffset();
370
371                 if (pos == 0)
372                         return -1;
373
374                 int p = firstNonWhitespaceBackward(document, pos - 1, partitioning, -1);
375
376                 if (p == -1)
377                         return -1;
378
379                 try {
380
381                         char ch = document.getChar(p);
382                         if (ch != '=' && ch != ']')
383                                 return -1;
384
385                         if (p == 0)
386                                 return offset;
387
388                         p = firstNonWhitespaceBackward(document, p - 1, partitioning, -1);
389                         if (p == -1)
390                                 return -1;
391
392                         ch = document.getChar(p);
393                         if (Scanner.isPHPIdentifierPart(ch) || ch == ']' || ch == '[')
394                                 return offset;
395
396                 } catch (BadLocationException e) {
397                 }
398                 return -1;
399         }
400
401         /**
402          * Computes an insert position for an opening brace if <code>offset</code>
403          * maps to a position in <code>document</code> involving a keyword taking
404          * a block after it. These are: <code>try</code>, <code>do</code>,
405          * <code>synchronized</code>, <code>static</code>,
406          * <code>finally</code>, or <code>else</code>.
407          * 
408          * @param document
409          *            the document being modified
410          * @param line
411          *            the current line under investigation
412          * @param offset
413          *            the offset of the caret position, relative to the line start.
414          * @return an insert position relative to the line start if
415          *         <code>line</code> contains one of the above keywords at or
416          *         before <code>offset</code>, -1 otherwise
417          */
418         private static int computeAfterTryDoElse(IDocument doc,
419                         ITextSelection line, int offset) {
420                 // search backward while WS, find 'try', 'do', 'else' in default
421                 // partition
422                 int p = offset + line.getOffset();
423                 p = firstWhitespaceToRight(doc, p);
424                 if (p == -1)
425                         return -1;
426                 p--;
427
428                 if (looksLike(doc, p, "try") //$NON-NLS-1$
429                                 || looksLike(doc, p, "do") //$NON-NLS-1$
430                                 || looksLike(doc, p, "synchronized") //$NON-NLS-1$
431                                 || looksLike(doc, p, "static") //$NON-NLS-1$
432                                 || looksLike(doc, p, "finally") //$NON-NLS-1$
433                                 || looksLike(doc, p, "else")) //$NON-NLS-1$
434                         return p + 1 - line.getOffset();
435
436                 return -1;
437         }
438
439         /**
440          * Computes an insert position for an opening brace if <code>offset</code>
441          * maps to a position in <code>document</code> with a expression in
442          * parenthesis that will take a block after the closing parenthesis.
443          * 
444          * @param document
445          *            the document being modified
446          * @param line
447          *            the current line under investigation
448          * @param offset
449          *            the offset of the caret position, relative to the line start.
450          * @param partitioning
451          *            the document partitioning
452          * @return an insert position relative to the line start if
453          *         <code>line</code> contains a parenthesized expression that can
454          *         be followed by a block, -1 otherwise
455          */
456         private static int computeAfterParenthesis(IDocument document,
457                         ITextSelection line, int offset, String partitioning) {
458                 // find the opening parenthesis for every closing parenthesis on the
459                 // current line after offset
460                 // return the position behind the closing parenthesis if it looks like a
461                 // method declaration
462                 // or an expression for an if, while, for, catch statement
463                 int pos = offset + line.getOffset();
464                 int length = line.getOffset() + line.getLength();
465                 int scanTo = scanForward(document, pos, partitioning, length, '}');
466                 if (scanTo == -1)
467                         scanTo = length;
468
469                 int closingParen = findClosingParenToLeft(document, pos, partitioning) - 1;
470
471                 while (true) {
472                         int startScan = closingParen + 1;
473                         closingParen = scanForward(document, startScan, partitioning,
474                                         scanTo, ')');
475                         if (closingParen == -1)
476                                 break;
477
478                         int openingParen = findOpeningParenMatch(document, closingParen,
479                                         partitioning);
480
481                         // no way an expression at the beginning of the document can mean
482                         // anything
483                         if (openingParen < 1)
484                                 break;
485
486                         // only select insert positions for parenthesis currently embracing
487                         // the caret
488                         if (openingParen > pos)
489                                 continue;
490
491                         if (looksLikeAnonymousClassDef(document, openingParen - 1,
492                                         partitioning))
493                                 return closingParen + 1 - line.getOffset();
494
495                         if (looksLikeIfWhileForCatch(document, openingParen - 1,
496                                         partitioning))
497                                 return closingParen + 1 - line.getOffset();
498
499                         if (looksLikeMethodDecl(document, openingParen - 1, partitioning))
500                                 return closingParen + 1 - line.getOffset();
501
502                 }
503
504                 return -1;
505         }
506
507         /**
508          * Finds a closing parenthesis to the left of <code>position</code> in
509          * document, where that parenthesis is only separated by whitespace from
510          * <code>position</code>. If no such parenthesis can be found,
511          * <code>position</code> is returned.
512          * 
513          * @param document
514          *            the document being modified
515          * @param position
516          *            the first character position in <code>document</code> to be
517          *            considered
518          * @param partitioning
519          *            the document partitioning
520          * @return the position of a closing parenthesis left to
521          *         <code>position</code> separated only by whitespace, or
522          *         <code>position</code> if no parenthesis can be found
523          */
524         private static int findClosingParenToLeft(IDocument document, int position,
525                         String partitioning) {
526                 final char CLOSING_PAREN = ')';
527                 try {
528                         if (position < 1)
529                                 return position;
530
531                         int nonWS = firstNonWhitespaceBackward(document, position - 1,
532                                         partitioning, -1);
533                         if (nonWS != -1 && document.getChar(nonWS) == CLOSING_PAREN)
534                                 return nonWS;
535                 } catch (BadLocationException e1) {
536                 }
537                 return position;
538         }
539
540         /**
541          * Finds the first whitespace character position to the right of (and
542          * including) <code>position</code>.
543          * 
544          * @param document
545          *            the document being modified
546          * @param position
547          *            the first character position in <code>document</code> to be
548          *            considered
549          * @return the position of a whitespace character greater or equal than
550          *         <code>position</code> separated only by whitespace, or -1 if
551          *         none found
552          */
553         private static int firstWhitespaceToRight(IDocument document, int position) {
554                 int length = document.getLength();
555                 Assert.isTrue(position >= 0);
556                 Assert.isTrue(position <= length);
557
558                 try {
559                         while (position < length) {
560                                 char ch = document.getChar(position);
561                                 if (Character.isWhitespace(ch))
562                                         return position;
563                                 position++;
564                         }
565                         return position;
566                 } catch (BadLocationException e) {
567                 }
568                 return -1;
569         }
570
571         /**
572          * Finds the highest position in <code>document</code> such that the
573          * position is &lt;= <code>position</code> and &gt; <code>bound</code>
574          * and <code>Character.isWhitespace(document.getChar(pos))</code>
575          * evaluates to <code>false</code> and the position is in the default
576          * partition.
577          * 
578          * @param document
579          *            the document being modified
580          * @param position
581          *            the first character position in <code>document</code> to be
582          *            considered
583          * @param partitioning
584          *            the document partitioning
585          * @param bound
586          *            the first position in <code>document</code> to not consider
587          *            any more, with <code>bound</code> &gt; <code>position</code>
588          * @return the highest position of one element in <code>chars</code> in [<code>position</code>,
589          *         <code>scanTo</code>) that resides in a Java partition, or
590          *         <code>-1</code> if none can be found
591          */
592         private static int firstNonWhitespaceBackward(IDocument document,
593                         int position, String partitioning, int bound) {
594                 Assert.isTrue(position < document.getLength());
595                 Assert.isTrue(bound >= -1);
596
597                 try {
598                         while (position > bound) {
599                                 char ch = document.getChar(position);
600                                 if (!Character.isWhitespace(ch)
601                                                 && isDefaultPartition(document, position, partitioning))
602                                         return position;
603                                 position--;
604                         }
605                 } catch (BadLocationException e) {
606                 }
607                 return -1;
608         }
609
610         /**
611          * Finds the smallest position in <code>document</code> such that the
612          * position is &gt;= <code>position</code> and &lt; <code>bound</code>
613          * and <code>Character.isWhitespace(document.getChar(pos))</code>
614          * evaluates to <code>false</code> and the position is in the default
615          * partition.
616          * 
617          * @param document
618          *            the document being modified
619          * @param position
620          *            the first character position in <code>document</code> to be
621          *            considered
622          * @param partitioning
623          *            the document partitioning
624          * @param bound
625          *            the first position in <code>document</code> to not consider
626          *            any more, with <code>bound</code> &gt; <code>position</code>
627          * @return the smallest position of one element in <code>chars</code> in [<code>position</code>,
628          *         <code>scanTo</code>) that resides in a Java partition, or
629          *         <code>-1</code> if none can be found
630          */
631         private static int firstNonWhitespaceForward(IDocument document,
632                         int position, String partitioning, int bound) {
633                 Assert.isTrue(position >= 0);
634                 Assert.isTrue(bound <= document.getLength());
635
636                 try {
637                         while (position < bound) {
638                                 char ch = document.getChar(position);
639                                 if (!Character.isWhitespace(ch)
640                                                 && isDefaultPartition(document, position, partitioning))
641                                         return position;
642                                 position++;
643                         }
644                 } catch (BadLocationException e) {
645                 }
646                 return -1;
647         }
648
649         /**
650          * Finds the highest position in <code>document</code> such that the
651          * position is &lt;= <code>position</code> and &gt; <code>bound</code>
652          * and <code>document.getChar(position) == ch</code> evaluates to
653          * <code>true</code> for at least one ch in <code>chars</code> and the
654          * position is in the default partition.
655          * 
656          * @param document
657          *            the document being modified
658          * @param position
659          *            the first character position in <code>document</code> to be
660          *            considered
661          * @param partitioning
662          *            the document partitioning
663          * @param bound
664          *            the first position in <code>document</code> to not consider
665          *            any more, with <code>scanTo</code> &gt;
666          *            <code>position</code>
667          * @param chars
668          *            an array of <code>char</code> to search for
669          * @return the highest position of one element in <code>chars</code> in (<code>bound</code>,
670          *         <code>position</code>] that resides in a Java partition, or
671          *         <code>-1</code> if none can be found
672          */
673         private static int scanBackward(IDocument document, int position,
674                         String partitioning, int bound, char[] chars) {
675                 Assert.isTrue(bound >= -1);
676                 Assert.isTrue(position < document.getLength());
677
678                 Arrays.sort(chars);
679
680                 try {
681                         while (position > bound) {
682
683                                 if (Arrays.binarySearch(chars, document.getChar(position)) >= 0
684                                                 && isDefaultPartition(document, position, partitioning))
685                                         return position;
686
687                                 position--;
688                         }
689                 } catch (BadLocationException e) {
690                 }
691                 return -1;
692         }
693
694         // /**
695         // * Finds the highest position in <code>document</code> such that the
696         // position is &lt;= <code>position</code>
697         // * and &gt; <code>bound</code> and <code>document.getChar(position) ==
698         // ch</code> evaluates to <code>true</code>
699         // * and the position is in the default partition.
700         // *
701         // * @param document the document being modified
702         // * @param position the first character position in <code>document</code>
703         // to be considered
704         // * @param bound the first position in <code>document</code> to not
705         // consider any more, with <code>scanTo</code> &gt; <code>position</code>
706         // * @param chars an array of <code>char</code> to search for
707         // * @return the highest position of one element in <code>chars</code> in
708         // [<code>position</code>, <code>scanTo</code>) that resides in a Java
709         // partition, or <code>-1</code> if none can be found
710         // */
711         // private static int scanBackward(IDocument document, int position, int
712         // bound, char ch) {
713         // return scanBackward(document, position, bound, new char[] {ch});
714         // }
715         //
716         /**
717          * Finds the lowest position in <code>document</code> such that the
718          * position is &gt;= <code>position</code> and &lt; <code>bound</code>
719          * and <code>document.getChar(position) == ch</code> evaluates to
720          * <code>true</code> for at least one ch in <code>chars</code> and the
721          * position is in the default partition.
722          * 
723          * @param document
724          *            the document being modified
725          * @param position
726          *            the first character position in <code>document</code> to be
727          *            considered
728          * @param partitioning
729          *            the document partitioning
730          * @param bound
731          *            the first position in <code>document</code> to not consider
732          *            any more, with <code>scanTo</code> &gt;
733          *            <code>position</code>
734          * @param chars
735          *            an array of <code>char</code> to search for
736          * @return the lowest position of one element in <code>chars</code> in [<code>position</code>,
737          *         <code>bound</code>) that resides in a Java partition, or
738          *         <code>-1</code> if none can be found
739          */
740         private static int scanForward(IDocument document, int position,
741                         String partitioning, int bound, char[] chars) {
742                 Assert.isTrue(position >= 0);
743                 Assert.isTrue(bound <= document.getLength());
744
745                 Arrays.sort(chars);
746
747                 try {
748                         while (position < bound) {
749
750                                 if (Arrays.binarySearch(chars, document.getChar(position)) >= 0
751                                                 && isDefaultPartition(document, position, partitioning))
752                                         return position;
753
754                                 position++;
755                         }
756                 } catch (BadLocationException e) {
757                 }
758                 return -1;
759         }
760
761         /**
762          * Finds the lowest position in <code>document</code> such that the
763          * position is &gt;= <code>position</code> and &lt; <code>bound</code>
764          * and <code>document.getChar(position) == ch</code> evaluates to
765          * <code>true</code> and the position is in the default partition.
766          * 
767          * @param document
768          *            the document being modified
769          * @param position
770          *            the first character position in <code>document</code> to be
771          *            considered
772          * @param partitioning
773          *            the document partitioning
774          * @param bound
775          *            the first position in <code>document</code> to not consider
776          *            any more, with <code>scanTo</code> &gt;
777          *            <code>position</code>
778          * @param chars
779          *            an array of <code>char</code> to search for
780          * @return the lowest position of one element in <code>chars</code> in [<code>position</code>,
781          *         <code>bound</code>) that resides in a Java partition, or
782          *         <code>-1</code> if none can be found
783          */
784         private static int scanForward(IDocument document, int position,
785                         String partitioning, int bound, char ch) {
786                 return scanForward(document, position, partitioning, bound,
787                                 new char[] { ch });
788         }
789
790         /**
791          * Checks whether the content of <code>document</code> in the range (<code>offset</code>,
792          * <code>length</code>) contains the <code>new</code> keyword.
793          * 
794          * @param document
795          *            the document being modified
796          * @param offset
797          *            the first character position in <code>document</code> to be
798          *            considered
799          * @param length
800          *            the length of the character range to be considered
801          * @param partitioning
802          *            the document partitioning
803          * @return <code>true</code> if the specified character range contains a
804          *         <code>new</code> keyword, <code>false</code> otherwise.
805          */
806         private static boolean isNewMatch(IDocument document, int offset,
807                         int length, String partitioning) {
808                 Assert.isTrue(length >= 0);
809                 Assert.isTrue(offset >= 0);
810                 Assert.isTrue(offset + length < document.getLength() + 1);
811
812                 try {
813                         String text = document.get(offset, length);
814                         int pos = text.indexOf("new"); //$NON-NLS-1$
815
816                         while (pos != -1
817                                         && !isDefaultPartition(document, pos + offset, partitioning))
818                                 pos = text.indexOf("new", pos + 2); //$NON-NLS-1$
819
820                         if (pos < 0)
821                                 return false;
822
823                         if (pos != 0 && Scanner.isPHPIdentifierPart(text.charAt(pos - 1)))
824                                 return false;
825
826                         if (pos + 3 < length
827                                         && Scanner.isPHPIdentifierPart(text.charAt(pos + 3)))
828                                 return false;
829
830                         return true;
831
832                 } catch (BadLocationException e) {
833                 }
834                 return false;
835         }
836
837         /**
838          * Checks whether the content of <code>document</code> at
839          * <code>position</code> looks like an anonymous class definition.
840          * <code>position</code> must be to the left of the opening parenthesis of
841          * the definition's parameter list.
842          * 
843          * @param document
844          *            the document being modified
845          * @param position
846          *            the first character position in <code>document</code> to be
847          *            considered
848          * @param partitioning
849          *            the document partitioning
850          * @return <code>true</code> if the content of <code>document</code>
851          *         looks like an anonymous class definition, <code>false</code>
852          *         otherwise
853          */
854         private static boolean looksLikeAnonymousClassDef(IDocument document,
855                         int position, String partitioning) {
856                 int previousCommaOrParen = scanBackward(document, position - 1,
857                                 partitioning, -1, new char[] { ',', '(' });
858                 if (previousCommaOrParen == -1 || position < previousCommaOrParen + 5) // 2
859                                                                                                                                                                 // for
860                                                                                                                                                                 // borders,
861                                                                                                                                                                 // 3
862                                                                                                                                                                 // for
863                                                                                                                                                                 // "new"
864                         return false;
865
866                 if (isNewMatch(document, previousCommaOrParen + 1, position
867                                 - previousCommaOrParen - 2, partitioning))
868                         return true;
869
870                 return false;
871         }
872
873         /**
874          * Checks whether <code>position</code> resides in a default (Java)
875          * partition of <code>document</code>.
876          * 
877          * @param document
878          *            the document being modified
879          * @param position
880          *            the position to be checked
881          * @param partitioning
882          *            the document partitioning
883          * @return <code>true</code> if <code>position</code> is in the default
884          *         partition of <code>document</code>, <code>false</code>
885          *         otherwise
886          */
887         private static boolean isDefaultPartition(IDocument document, int position,
888                         String partitioning) {
889                 Assert.isTrue(position >= 0);
890                 Assert.isTrue(position <= document.getLength());
891
892                 try {
893                         // don't use getPartition2 since we're interested in the scanned
894                         // character's partition
895                         ITypedRegion region = TextUtilities.getPartition(document,
896                                         partitioning, position, false);
897                         return region.getType().equals(IDocument.DEFAULT_CONTENT_TYPE);
898
899                 } catch (BadLocationException e) {
900                 }
901
902                 return false;
903         }
904
905         /**
906          * Finds the position of the parenthesis matching the closing parenthesis at
907          * <code>position</code>.
908          * 
909          * @param document
910          *            the document being modified
911          * @param position
912          *            the position in <code>document</code> of a closing
913          *            parenthesis
914          * @param partitioning
915          *            the document partitioning
916          * @return the position in <code>document</code> of the matching
917          *         parenthesis, or -1 if none can be found
918          */
919         private static int findOpeningParenMatch(IDocument document, int position,
920                         String partitioning) {
921                 final char CLOSING_PAREN = ')';
922                 final char OPENING_PAREN = '(';
923
924                 Assert.isTrue(position < document.getLength());
925                 Assert.isTrue(position >= 0);
926                 Assert.isTrue(isDefaultPartition(document, position, partitioning));
927
928                 try {
929
930                         Assert.isTrue(document.getChar(position) == CLOSING_PAREN);
931
932                         int depth = 1;
933                         while (true) {
934                                 position = scanBackward(document, position - 1, partitioning,
935                                                 -1, new char[] { CLOSING_PAREN, OPENING_PAREN });
936                                 if (position == -1)
937                                         return -1;
938
939                                 if (document.getChar(position) == CLOSING_PAREN)
940                                         depth++;
941                                 else
942                                         depth--;
943
944                                 if (depth == 0)
945                                         return position;
946                         }
947
948                 } catch (BadLocationException e) {
949                         return -1;
950                 }
951         }
952
953         /**
954          * Checks whether, to the left of <code>position</code> and separated only
955          * by whitespace, <code>document</code> contains a keyword taking a
956          * parameter list and a block after it. These are: <code>if</code>,
957          * <code>while</code>, <code>catch</code>, <code>for</code>,
958          * <code>synchronized</code>, <code>switch</code>.
959          * 
960          * @param document
961          *            the document being modified
962          * @param position
963          *            the first character position in <code>document</code> to be
964          *            considered
965          * @param partitioning
966          *            the document partitioning
967          * @return <code>true</code> if <code>document</code> contains any of
968          *         the above keywords to the left of <code>position</code>,
969          *         <code>false</code> otherwise
970          */
971         private static boolean looksLikeIfWhileForCatch(IDocument document,
972                         int position, String partitioning) {
973                 position = firstNonWhitespaceBackward(document, position, partitioning,
974                                 -1);
975                 if (position == -1)
976                         return false;
977
978                 return looksLike(document, position, "if") //$NON-NLS-1$
979                                 || looksLike(document, position, "while") //$NON-NLS-1$
980                                 || looksLike(document, position, "catch") //$NON-NLS-1$
981                                 || looksLike(document, position, "synchronized") //$NON-NLS-1$
982                                 || looksLike(document, position, "switch") //$NON-NLS-1$
983                                 || looksLike(document, position, "for"); //$NON-NLS-1$
984         }
985
986         /**
987          * Checks whether code>document</code> contains the <code>String</code> <code>like</code>
988          * such that its last character is at <code>position</code>. If <code>like</code>
989          * starts with a identifier part (as determined by
990          * {@link Scanner#isPHPIdentifierPart(char)}), it is also made sure that
991          * <code>like</code> is preceded by some non-identifier character or
992          * stands at the document start.
993          * 
994          * @param document
995          *            the document being modified
996          * @param position
997          *            the first character position in <code>document</code> to be
998          *            considered
999          * @param like
1000          *            the <code>String</code> to look for.
1001          * @return <code>true</code> if <code>document</code> contains <code>like</code>
1002          *         such that it ends at <code>position</code>, <code>false</code>
1003          *         otherwise
1004          */
1005         private static boolean looksLike(IDocument document, int position,
1006                         String like) {
1007                 int length = like.length();
1008                 if (position < length - 1)
1009                         return false;
1010
1011                 try {
1012                         if (!like.equals(document.get(position - length + 1, length)))
1013                                 return false;
1014
1015                         if (position >= length
1016                                         && Scanner.isPHPIdentifierPart(like.charAt(0))
1017                                         && Scanner.isPHPIdentifierPart(document.getChar(position
1018                                                         - length)))
1019                                 return false;
1020
1021                 } catch (BadLocationException e) {
1022                         return false;
1023                 }
1024
1025                 return true;
1026         }
1027
1028         /**
1029          * Checks whether the content of <code>document</code> at
1030          * <code>position</code> looks like a method declaration header (i.e. only
1031          * the return type and method name). <code>position</code> must be just
1032          * left of the opening parenthesis of the parameter list.
1033          * 
1034          * @param document
1035          *            the document being modified
1036          * @param position
1037          *            the first character position in <code>document</code> to be
1038          *            considered
1039          * @param partitioning
1040          *            the document partitioning
1041          * @return <code>true</code> if the content of <code>document</code>
1042          *         looks like a method definition, <code>false</code> otherwise
1043          */
1044         private static boolean looksLikeMethodDecl(IDocument document,
1045                         int position, String partitioning) {
1046
1047                 // method name
1048                 position = eatIdentToLeft(document, position, partitioning);
1049                 if (position < 1)
1050                         return false;
1051
1052                 position = eatBrackets(document, position - 1, partitioning);
1053                 if (position < 1)
1054                         return false;
1055
1056                 position = eatIdentToLeft(document, position - 1, partitioning);
1057
1058                 return position != -1;
1059         }
1060
1061         /**
1062          * From <code>position</code> to the left, eats any whitespace and then a
1063          * pair of brackets as used to declare an array return type like
1064          * 
1065          * <pre>
1066          * String [ ]
1067          * </pre>. The return value is either the position of the opening bracket
1068          * or <code>position</code> if no pair of brackets can be parsed.
1069          * 
1070          * @param document
1071          *            the document being modified
1072          * @param position
1073          *            the first character position in <code>document</code> to be
1074          *            considered
1075          * @param partitioning
1076          *            the document partitioning
1077          * @return the smallest character position of bracket pair or
1078          *         <code>position</code>
1079          */
1080         private static int eatBrackets(IDocument document, int position,
1081                         String partitioning) {
1082                 // accept array return type
1083                 int pos = firstNonWhitespaceBackward(document, position, partitioning,
1084                                 -1);
1085                 try {
1086                         if (pos > 1 && document.getChar(pos) == ']') {
1087                                 pos = firstNonWhitespaceBackward(document, pos - 1,
1088                                                 partitioning, -1);
1089                                 if (pos > 0 && document.getChar(pos) == '[')
1090                                         return pos;
1091                         }
1092                 } catch (BadLocationException e) {
1093                         // won't happen
1094                 }
1095                 return position;
1096         }
1097
1098         /**
1099          * From <code>position</code> to the left, eats any whitespace and the
1100          * first identifier, returning the position of the first identifier
1101          * character (in normal read order).
1102          * <p>
1103          * When called on a document with content <code>" some string  "</code> and
1104          * positionition 13, the return value will be 6 (the first letter in
1105          * <code>string</code>).
1106          * </p>
1107          * 
1108          * @param document
1109          *            the document being modified
1110          * @param position
1111          *            the first character position in <code>document</code> to be
1112          *            considered
1113          * @param partitioning
1114          *            the document partitioning
1115          * @return the smallest character position of an identifier or -1 if none
1116          *         can be found; always &lt;= <code>position</code>
1117          */
1118         private static int eatIdentToLeft(IDocument document, int position,
1119                         String partitioning) {
1120                 if (position < 0)
1121                         return -1;
1122                 Assert.isTrue(position < document.getLength());
1123
1124                 int p = firstNonWhitespaceBackward(document, position, partitioning, -1);
1125                 if (p == -1)
1126                         return -1;
1127
1128                 try {
1129                         while (p >= 0) {
1130
1131                                 char ch = document.getChar(p);
1132                                 if (Scanner.isPHPIdentifierPart(ch)) {
1133                                         p--;
1134                                         continue;
1135                                 }
1136
1137                                 // length must be > 0
1138                                 if (Character.isWhitespace(ch) && p != position)
1139                                         return p + 1;
1140                                 else
1141                                         return -1;
1142
1143                         }
1144
1145                         // start of document reached
1146                         return 0;
1147
1148                 } catch (BadLocationException e) {
1149                 }
1150                 return -1;
1151         }
1152
1153         /**
1154          * Returns a position in the first java partition after the last non-empty
1155          * and non-comment partition. There is no non-whitespace from the returned
1156          * position to the end of the partition it is contained in.
1157          * 
1158          * @param document
1159          *            the document being modified
1160          * @param line
1161          *            the line under investigation
1162          * @param offset
1163          *            the caret offset into <code>line</code>
1164          * @param partitioning
1165          *            the document partitioning
1166          * @return the position of the next Java partition, or the end of
1167          *         <code>line</code>
1168          */
1169         private static int nextPartitionOrLineEnd(IDocument document,
1170                         ITextSelection line, int offset, String partitioning) {
1171                 // run relative to document
1172                 final int docOffset = offset + line.getOffset();
1173                 final int eol = line.getOffset() + line.getLength();
1174                 int nextPartitionPos = eol; // init with line end
1175                 int validPosition = docOffset;
1176
1177                 try {
1178                         ITypedRegion partition = TextUtilities.getPartition(document,
1179                                         partitioning, nextPartitionPos, true);
1180                         validPosition = getValidPositionForPartition(document, partition,
1181                                         eol);
1182                         while (validPosition == -1) {
1183                                 nextPartitionPos = partition.getOffset() - 1;
1184                                 if (nextPartitionPos < docOffset) {
1185                                         validPosition = docOffset;
1186                                         break;
1187                                 }
1188                                 partition = TextUtilities.getPartition(document, partitioning,
1189                                                 nextPartitionPos, false);
1190                                 validPosition = getValidPositionForPartition(document,
1191                                                 partition, eol);
1192                         }
1193                 } catch (BadLocationException e) {
1194                 }
1195
1196                 validPosition = Math.max(validPosition, docOffset);
1197                 // make relative to line
1198                 validPosition -= line.getOffset();
1199                 return validPosition;
1200         }
1201
1202         /**
1203          * Returns a valid insert location (except for whitespace) in
1204          * <code>partition</code> or -1 if there is no valid insert location. An
1205          * valid insert location is right after any java string or character
1206          * partition, or at the end of a java default partition, but never behind
1207          * <code>maxOffset</code>. Comment partitions or empty java partitions do
1208          * never yield valid insert positions.
1209          * 
1210          * @param doc
1211          *            the document being modified
1212          * @param partition
1213          *            the current partition
1214          * @param maxOffset
1215          *            the maximum offset to consider
1216          * @return a valid insert location in <code>partition</code>, or -1 if
1217          *         there is no valid insert location
1218          */
1219         private static int getValidPositionForPartition(IDocument doc,
1220                         ITypedRegion partition, int maxOffset) {
1221                 final int INVALID = -1;
1222
1223                 if (IPHPPartitions.PHP_PHPDOC_COMMENT.equals(partition.getType()))
1224                         return INVALID;
1225                 if (IPHPPartitions.PHP_MULTILINE_COMMENT.equals(partition.getType()))
1226                         return INVALID;
1227                 if (IPHPPartitions.PHP_SINGLELINE_COMMENT.equals(partition.getType()))
1228                         return INVALID;
1229
1230                 int endOffset = Math.min(maxOffset, partition.getOffset()
1231                                 + partition.getLength());
1232
1233                 // if (IPHPPartitions.JAVA_CHARACTER.equals(partition.getType()))
1234                 // return endOffset;
1235                 if (IPHPPartitions.PHP_STRING_DQ.equals(partition.getType()))
1236                         return endOffset;
1237                 if (IPHPPartitions.PHP_STRING_SQ.equals(partition.getType()))
1238                         return endOffset;
1239                 if (IPHPPartitions.PHP_STRING_HEREDOC.equals(partition.getType()))
1240                         return endOffset;
1241                 if (IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())) {
1242                         try {
1243                                 if (doc.get(partition.getOffset(),
1244                                                 endOffset - partition.getOffset()).trim().length() == 0)
1245                                         return INVALID;
1246                                 else
1247                                         return endOffset;
1248                         } catch (BadLocationException e) {
1249                                 return INVALID;
1250                         }
1251                 }
1252                 // default: we don't know anything about the partition - assume valid
1253                 return endOffset;
1254         }
1255
1256         /**
1257          * Determines whether the current line contains a for statement. Algorithm:
1258          * any "for" word in the line is a positive, "for" contained in a string
1259          * literal will produce a false positive.
1260          * 
1261          * @param line
1262          *            the line where the change is being made
1263          * @param offset
1264          *            the position of the caret
1265          * @return <code>true</code> if <code>line</code> contains
1266          *         <code>for</code>, <code>false</code> otherwise
1267          */
1268         private static boolean isForStatement(String line, int offset) {
1269                 /* searching for (^|\s)for(\s|$) */
1270                 int forPos = line.indexOf("for"); //$NON-NLS-1$
1271                 if (forPos != -1) {
1272                         if ((forPos == 0 || !Scanner.isPHPIdentifierPart(line
1273                                         .charAt(forPos - 1)))
1274                                         && (line.length() == forPos + 3 || !Scanner
1275                                                         .isPHPIdentifierPart(line.charAt(forPos + 3))))
1276                                 return true;
1277                 }
1278                 return false;
1279         }
1280
1281         /**
1282          * Returns the position in <code>text</code> after which there comes only
1283          * whitespace, up to <code>offset</code>.
1284          * 
1285          * @param text
1286          *            the text being searched
1287          * @param offset
1288          *            the maximum offset to search for
1289          * @return the smallest value <code>v</code> such that
1290          *         <code>text.substring(v, offset).trim() == 0</code>
1291          */
1292         private static int startOfWhitespaceBeforeOffset(String text, int offset) {
1293                 int i = Math.min(offset, text.length());
1294                 for (; i >= 1; i--) {
1295                         if (!Character.isWhitespace(text.charAt(i - 1)))
1296                                 break;
1297                 }
1298                 return i;
1299         }
1300 }