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