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