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