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