Fixed: 1774625 - duplicate one function block down into a folded php-doc
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpdt / internal / ui / text / JavaHeuristicScanner.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.phpeclipse.phpeditor.php.PHPDocumentPartitioner;
17
18 import org.eclipse.jface.text.Assert;
19 import org.eclipse.jface.text.BadLocationException;
20 import org.eclipse.jface.text.IDocument;
21 import org.eclipse.jface.text.IRegion;
22 import org.eclipse.jface.text.ITypedRegion;
23 import org.eclipse.jface.text.Region;
24 import org.eclipse.jface.text.TextUtilities;
25
26 /**
27  * Utility methods for heuristic based Java manipulations in an incomplete Java
28  * source file.
29  * 
30  * <p>
31  * An instance holds some internal position in the document and is therefore not
32  * threadsafe.
33  * </p>
34  * 
35  * @since 3.0
36  */
37 public class JavaHeuristicScanner implements Symbols {
38         /**
39          * Returned by all methods when the requested position could not be found,
40          * or if a {@link BadLocationException} was thrown while scanning.
41          */
42         public static final int NOT_FOUND = -1;
43
44         /**
45          * Special bound parameter that means either -1 (backward scanning) or
46          * <code>fDocument.getLength()</code> (forward scanning).
47          */
48         public static final int UNBOUND = -2;
49
50         /* character constants */
51         private static final char LBRACE = '{';
52
53         private static final char RBRACE = '}';
54
55         private static final char LPAREN = '(';
56
57         private static final char RPAREN = ')';
58
59         private static final char SEMICOLON = ';';
60
61         private static final char COLON = ':';
62
63         private static final char COMMA = ',';
64
65         private static final char LBRACKET = '[';
66
67         private static final char RBRACKET = ']';
68
69         private static final char QUESTIONMARK = '?';
70
71         private static final char EQUAL = '=';
72
73         /**
74          * Specifies the stop condition, upon which the <code>scanXXX</code>
75          * methods will decide whether to keep scanning or not. This interface may
76          * implemented by clients.
77          */
78         public interface StopCondition {
79                 /**
80                  * Instructs the scanner to return the current position.
81                  * 
82                  * @param ch
83                  *            the char at the current position
84                  * @param position
85                  *            the current position
86                  * @param forward
87                  *            the iteration direction
88                  * @return <code>true</code> if the stop condition is met.
89                  */
90                 boolean stop(char ch, int position, boolean forward);
91         }
92
93         /**
94          * Stops upon a non-whitespace (as defined by
95          * {@link Character#isWhitespace(char)}) character.
96          */
97         private static class NonWhitespace implements StopCondition {
98                 /*
99                  * @see net.sourceforge.phpdt.internal.ui.text.JavaHeuristicScanner.StopCondition#stop(char)
100                  */
101                 public boolean stop(char ch, int position, boolean forward) {
102                         return !Character.isWhitespace(ch);
103                 }
104         }
105
106         /**
107          * Stops upon a non-whitespace character in the default partition.
108          * 
109          * @see NonWhitespace
110          */
111         private class NonWhitespaceDefaultPartition extends NonWhitespace {
112                 /*
113                  * @see net.sourceforge.phpdt.internal.ui.text.JavaHeuristicScanner.StopCondition#stop(char)
114                  */
115                 public boolean stop(char ch, int position, boolean forward) {
116                         return super.stop(ch, position, true)
117                                         && isDefaultPartition(position);
118                 }
119         }
120
121         /**
122          * Stops upon a non-java identifier (as defined by
123          * {@link Scanner#isPHPIdentifierPart(char)}) character.
124          */
125         private static class NonJavaIdentifierPart implements StopCondition {
126                 /*
127                  * @see net.sourceforge.phpdt.internal.ui.text.JavaHeuristicScanner.StopCondition#stop(char)
128                  */
129                 public boolean stop(char ch, int position, boolean forward) {
130                         return !Scanner.isPHPIdentifierPart(ch);
131                 }
132         }
133
134         /**
135          * Stops upon a non-java identifier character in the default partition.
136          * 
137          * @see NonJavaIdentifierPart
138          */
139         private class NonJavaIdentifierPartDefaultPartition extends
140                         NonJavaIdentifierPart {
141                 /*
142                  * @see net.sourceforge.phpdt.internal.ui.text.JavaHeuristicScanner.StopCondition#stop(char)
143                  */
144                 public boolean stop(char ch, int position, boolean forward) {
145                         return super.stop(ch, position, true)
146                                         || !isDefaultPartition(position);
147                 }
148         }
149
150         /**
151          * Stops upon a character in the default partition that matches the given
152          * character list.
153          */
154         private class CharacterMatch implements StopCondition {
155                 private final char[] fChars;
156
157                 /**
158                  * Creates a new instance.
159                  * 
160                  * @param ch
161                  *            the single character to match
162                  */
163                 public CharacterMatch(char ch) {
164                         this(new char[] { ch });
165                 }
166
167                 /**
168                  * Creates a new instance.
169                  * 
170                  * @param chars
171                  *            the chars to match.
172                  */
173                 public CharacterMatch(char[] chars) {
174                         Assert.isNotNull(chars);
175                         Assert.isTrue(chars.length > 0);
176                         fChars = chars;
177                         Arrays.sort(chars);
178                 }
179
180                 /*
181                  * @see net.sourceforge.phpdt.internal.ui.text.JavaHeuristicScanner.StopCondition#stop(char,
182                  *      int)
183                  */
184                 public boolean stop(char ch, int position, boolean forward) {
185                         return Arrays.binarySearch(fChars, ch) >= 0
186                                         && isDefaultPartition(position);
187                 }
188         }
189
190         /**
191          * Acts like character match, but skips all scopes introduced by
192          * parenthesis, brackets, and braces.
193          */
194         protected class SkippingScopeMatch extends CharacterMatch {
195                 private char fOpening, fClosing;
196
197                 private int fDepth = 0;
198
199                 /**
200                  * Creates a new instance.
201                  * 
202                  * @param ch
203                  *            the single character to match
204                  */
205                 public SkippingScopeMatch(char ch) {
206                         super(ch);
207                 }
208
209                 /**
210                  * Creates a new instance.
211                  * 
212                  * @param chars
213                  *            the chars to match.
214                  */
215                 public SkippingScopeMatch(char[] chars) {
216                         super(chars);
217                 }
218
219                 /*
220                  * @see net.sourceforge.phpdt.internal.ui.text.JavaHeuristicScanner.StopCondition#stop(char,
221                  *      int)
222                  */
223                 public boolean stop(char ch, int position, boolean forward) {
224
225                         if (fDepth == 0 && super.stop(ch, position, true))
226                                 return true;
227                         else if (ch == fOpening)
228                                 fDepth++;
229                         else if (ch == fClosing) {
230                                 fDepth--;
231                                 if (fDepth == 0) {
232                                         fOpening = 0;
233                                         fClosing = 0;
234                                 }
235                         } else if (fDepth == 0) {
236                                 fDepth = 1;
237                                 if (forward) {
238
239                                         switch (ch) {
240                                         case LBRACE:
241                                                 fOpening = LBRACE;
242                                                 fClosing = RBRACE;
243                                                 break;
244                                         case LPAREN:
245                                                 fOpening = LPAREN;
246                                                 fClosing = RPAREN;
247                                                 break;
248                                         case LBRACKET:
249                                                 fOpening = LBRACKET;
250                                                 fClosing = RBRACKET;
251                                                 break;
252                                         }
253
254                                 } else {
255                                         switch (ch) {
256                                         case RBRACE:
257                                                 fOpening = RBRACE;
258                                                 fClosing = LBRACE;
259                                                 break;
260                                         case RPAREN:
261                                                 fOpening = RPAREN;
262                                                 fClosing = LPAREN;
263                                                 break;
264                                         case RBRACKET:
265                                                 fOpening = RBRACKET;
266                                                 fClosing = LBRACKET;
267                                                 break;
268                                         }
269
270                                 }
271                         }
272
273                         return false;
274
275                 }
276
277         }
278
279         /** The document being scanned. */
280         private IDocument fDocument;
281
282         /** The partitioning being used for scanning. */
283         private String fPartitioning;
284
285         /** The partition to scan in. */
286         private String fPartition;
287
288         /* internal scan state */
289
290         /** the most recently read character. */
291         private char fChar;
292
293         /** the most recently read position. */
294         private int fPos;
295
296         /* preset stop conditions */
297         private final StopCondition fNonWSDefaultPart = new NonWhitespaceDefaultPartition();
298
299         private final static StopCondition fNonWS = new NonWhitespace();
300
301         private final StopCondition fNonIdent = new NonJavaIdentifierPartDefaultPartition();
302
303         /**
304          * Creates a new instance.
305          * 
306          * @param document
307          *            the document to scan
308          * @param partitioning
309          *            the partitioning to use for scanning
310          * @param partition
311          *            the partition to scan in
312          */
313         public JavaHeuristicScanner(IDocument document, String partitioning,
314                         String partition) {
315                 Assert.isNotNull(document);
316                 Assert.isNotNull(partitioning);
317                 Assert.isNotNull(partition);
318                 fDocument = document;
319                 fPartitioning = partitioning;
320                 fPartition = partition;
321         }
322
323         /**
324          * Calls
325          * <code>this(document, IJavaPartitions.JAVA_PARTITIONING, IDocument.DEFAULT_CONTENT_TYPE)</code>.
326          * 
327          * @param document
328          *            the document to scan.
329          */
330         public JavaHeuristicScanner(IDocument document) {
331 //              this(document, IPHPPartitions.PHP_PARTITIONING,
332 //                              IDocument.DEFAULT_CONTENT_TYPE);
333                 this(document, IPHPPartitions.PHP_PARTITIONING,
334                                 PHPDocumentPartitioner.PHP_SCRIPT_CODE);
335         }
336
337         /**
338          * Returns the most recent internal scan position.
339          * 
340          * @return the most recent internal scan position.
341          */
342         public int getPosition() {
343                 return fPos;
344         }
345
346         /**
347          * Returns the next token in forward direction, starting at
348          * <code>start</code>, and not extending further than <code>bound</code>.
349          * The return value is one of the constants defined in {@link Symbols}.
350          * After a call, {@link #getPosition()} will return the position just after
351          * the scanned token (i.e. the next position that will be scanned).
352          * 
353          * @param start
354          *            the first character position in the document to consider
355          * @param bound
356          *            the first position not to consider any more
357          * @return a constant from {@link Symbols} describing the next token
358          */
359         public int nextToken(int start, int bound) {
360                 int pos = scanForward(start, bound, fNonWSDefaultPart);
361                 if (pos == NOT_FOUND)
362                         return TokenEOF;
363
364                 fPos++;
365
366                 switch (fChar) {
367                 case LBRACE:
368                         return TokenLBRACE;
369                 case RBRACE:
370                         return TokenRBRACE;
371                 case LBRACKET:
372                         return TokenLBRACKET;
373                 case RBRACKET:
374                         return TokenRBRACKET;
375                 case LPAREN:
376                         return TokenLPAREN;
377                 case RPAREN:
378                         return TokenRPAREN;
379                 case SEMICOLON:
380                         return TokenSEMICOLON;
381                 case COMMA:
382                         return TokenCOMMA;
383                 case QUESTIONMARK:
384                         return TokenQUESTIONMARK;
385                 case EQUAL:
386                         try {
387                                 if (fDocument.getChar(fPos) == '>') {
388                                         fPos++;
389                                         return TokenOTHER;
390                                 }
391                         } catch (BadLocationException e) {
392                         }
393                         return TokenEQUAL;
394                 case '<':
395                         try {
396                                 if (fDocument.get(fPos, 4).equalsIgnoreCase("?php")) {
397                                         fPos += 4;
398                                         return TokenEOF;
399                                 } else if (fDocument.getChar(fPos) == '?') {
400                                         fPos++;
401                                         return TokenEOF;
402                                 }
403                         } catch (BadLocationException e) {
404                         }
405                 }
406
407                 // else
408                 if (Scanner.isPHPIdentifierPart(fChar)) {
409                         // assume an ident or keyword
410                         int from = pos, to;
411                         pos = scanForward(pos + 1, bound, fNonIdent);
412                         if (pos == NOT_FOUND)
413                                 to = bound == UNBOUND ? fDocument.getLength() : bound;
414                         else
415                                 to = pos;
416
417                         String identOrKeyword;
418                         try {
419                                 identOrKeyword = fDocument.get(from, to - from);
420                         } catch (BadLocationException e) {
421                                 return TokenEOF;
422                         }
423
424                         return getToken(identOrKeyword);
425
426                 } else {
427                         // operators, number literals etc
428                         return TokenOTHER;
429                 }
430         }
431
432         /**
433          * Returns the next token in backward direction, starting at
434          * <code>start</code>, and not extending further than <code>bound</code>.
435          * The return value is one of the constants defined in {@link Symbols}.
436          * After a call, {@link #getPosition()} will return the position just before
437          * the scanned token starts (i.e. the next position that will be scanned).
438          * 
439          * @param start
440          *            the first character position in the document to consider
441          * @param bound
442          *            the first position not to consider any more
443          * @return a constant from {@link Symbols} describing the previous token
444          */
445         public int previousToken(int start, int bound) {
446                 int pos = scanBackward(start, bound, fNonWSDefaultPart);
447                 if (pos == NOT_FOUND)
448                         return TokenEOF;
449
450                 fPos--;
451
452                 switch (fChar) {
453                 case LBRACE:
454                         return TokenLBRACE;
455                 case RBRACE:
456                         return TokenRBRACE;
457                 case LBRACKET:
458                         return TokenLBRACKET;
459                 case RBRACKET:
460                         return TokenRBRACKET;
461                 case LPAREN:
462                         return TokenLPAREN;
463                 case RPAREN:
464                         return TokenRPAREN;
465                 case SEMICOLON:
466                         return TokenSEMICOLON;
467                 case COLON:
468                         return TokenCOLON;
469                 case COMMA:
470                         return TokenCOMMA;
471                 case QUESTIONMARK:
472                         return TokenQUESTIONMARK;
473                 case EQUAL:
474                         return TokenEQUAL;
475                 case '>':
476                         try {
477                                 switch (fDocument.getChar(fPos)) {
478                                 case '=':
479                                         fPos--;
480                                         return TokenOTHER;
481                                 case '?':
482                                         fPos--;
483                                         return TokenEOF;
484                                 }
485                         } catch (BadLocationException e) {
486                         }
487                 }
488
489                 // else
490                 if (Scanner.isPHPIdentifierPart(fChar)) {
491                         // assume an ident or keyword
492                         int from, to = pos + 1;
493                         pos = scanBackward(pos - 1, bound, fNonIdent);
494                         if (pos == NOT_FOUND)
495                                 from = bound == UNBOUND ? 0 : bound + 1;
496                         else
497                                 from = pos + 1;
498
499                         String identOrKeyword;
500                         try {
501                                 identOrKeyword = fDocument.get(from, to - from);
502                         } catch (BadLocationException e) {
503                                 return TokenEOF;
504                         }
505
506                         return getToken(identOrKeyword);
507
508                 } else {
509                         // operators, number literals etc
510                         return TokenOTHER;
511                 }
512
513         }
514
515         /**
516          * Returns one of the keyword constants or <code>TokenIDENT</code> for a
517          * scanned identifier.
518          * 
519          * @param s
520          *            a scanned identifier
521          * @return one of the constants defined in {@link Symbols}
522          */
523         private int getToken(String s) {
524                 Assert.isNotNull(s);
525
526                 switch (s.length()) {
527                 case 2:
528                         if ("if".equals(s)) //$NON-NLS-1$
529                                 return TokenIF;
530                         if ("do".equals(s)) //$NON-NLS-1$
531                                 return TokenDO;
532                         break;
533                 case 3:
534                         if ("for".equals(s)) //$NON-NLS-1$
535                                 return TokenFOR;
536                         if ("try".equals(s)) //$NON-NLS-1$
537                                 return TokenTRY;
538                         if ("new".equals(s)) //$NON-NLS-1$
539                                 return TokenNEW;
540                         break;
541                 case 4:
542                         if ("case".equals(s)) //$NON-NLS-1$
543                                 return TokenCASE;
544                         if ("else".equals(s)) //$NON-NLS-1$
545                                 return TokenELSE;
546                         if ("goto".equals(s)) //$NON-NLS-1$
547                                 return TokenGOTO;
548                         break;
549                 case 5:
550                         if ("break".equals(s)) //$NON-NLS-1$
551                                 return TokenBREAK;
552                         if ("catch".equals(s)) //$NON-NLS-1$
553                                 return TokenCATCH;
554                         if ("while".equals(s)) //$NON-NLS-1$
555                                 return TokenWHILE;
556                         break;
557                 case 6:
558                         if ("return".equals(s)) //$NON-NLS-1$
559                                 return TokenRETURN;
560                         if ("static".equals(s)) //$NON-NLS-1$
561                                 return TokenSTATIC;
562                         if ("switch".equals(s)) //$NON-NLS-1$
563                                 return TokenSWITCH;
564                         break;
565                 case 7:
566                         if ("default".equals(s)) //$NON-NLS-1$
567                                 return TokenDEFAULT;
568                         if ("finally".equals(s)) //$NON-NLS-1$
569                                 return TokenFINALLY;
570                         break;
571                 case 12:
572                         if ("synchronized".equals(s)) //$NON-NLS-1$
573                                 return TokenSYNCHRONIZED;
574                         break;
575                 }
576                 return TokenIDENT;
577         }
578
579         /**
580          * Returns the position of the closing peer character (forward search). Any
581          * scopes introduced by opening peers are skipped. All peers accounted for
582          * must reside in the default partition.
583          * 
584          * <p>
585          * Note that <code>start</code> must not point to the opening peer, but to
586          * the first character being searched.
587          * </p>
588          * 
589          * @param start
590          *            the start position
591          * @param openingPeer
592          *            the opening peer character (e.g. '{')
593          * @param closingPeer
594          *            the closing peer character (e.g. '}')
595          * @return the matching peer character position, or <code>NOT_FOUND</code>
596          */
597         public int findClosingPeer(int start, final char openingPeer,
598                         final char closingPeer) {
599                 Assert.isNotNull(fDocument);
600                 Assert.isTrue(start >= 0);
601
602                 try {
603                         int depth = 1;
604                         start -= 1;
605                         while (true) {
606                                 start = scanForward(start + 1, UNBOUND, new CharacterMatch(
607                                                 new char[] { openingPeer, closingPeer }));
608                                 if (start == NOT_FOUND)
609                                         return NOT_FOUND;
610
611                                 if (fDocument.getChar(start) == openingPeer)
612                                         depth++;
613                                 else
614                                         depth--;
615
616                                 if (depth == 0)
617                                         return start;
618                         }
619
620                 } catch (BadLocationException e) {
621                         return NOT_FOUND;
622                 }
623         }
624
625         /**
626          * Returns the position of the opening peer character (backward search). Any
627          * scopes introduced by closing peers are skipped. All peers accounted for
628          * must reside in the default partition.
629          * 
630          * <p>
631          * Note that <code>start</code> must not point to the closing peer, but to
632          * the first character being searched.
633          * </p>
634          * 
635          * @param start
636          *            the start position
637          * @param openingPeer
638          *            the opening peer character (e.g. '{')
639          * @param closingPeer
640          *            the closing peer character (e.g. '}')
641          * @return the matching peer character position, or <code>NOT_FOUND</code>
642          */
643         public int findOpeningPeer(int start, char openingPeer, char closingPeer) {
644                 Assert.isTrue(start < fDocument.getLength());
645
646                 try {
647                         int depth = 1;
648                         start += 1;
649                         while (true) {
650                                 start = scanBackward(start - 1, UNBOUND, new CharacterMatch(
651                                                 new char[] { openingPeer, closingPeer }));
652                                 if (start == NOT_FOUND)
653                                         return NOT_FOUND;
654
655                                 if (fDocument.getChar(start) == closingPeer)
656                                         depth++;
657                                 else
658                                         depth--;
659
660                                 if (depth == 0)
661                                         return start;
662                         }
663
664                 } catch (BadLocationException e) {
665                         return NOT_FOUND;
666                 }
667         }
668
669         /**
670          * Computes the surrounding block around <code>offset</code>. The search
671          * is started at the beginning of <code>offset</code>, i.e. an opening
672          * brace at <code>offset</code> will not be part of the surrounding block,
673          * but a closing brace will.
674          * 
675          * @param offset
676          *            the offset for which the surrounding block is computed
677          * @return a region describing the surrounding block, or <code>null</code>
678          *         if none can be found
679          */
680         public IRegion findSurroundingBlock(int offset) {
681                 if (offset < 1 || offset >= fDocument.getLength())
682                         return null;
683
684                 int begin = findOpeningPeer(offset - 1, LBRACE, RBRACE);
685                 int end = findClosingPeer(offset, LBRACE, RBRACE);
686                 if (begin == NOT_FOUND || end == NOT_FOUND)
687                         return null;
688                 return new Region(begin, end + 1 - begin);
689         }
690
691         /**
692          * Finds the smallest position in <code>fDocument</code> such that the
693          * position is &gt;= <code>position</code> and &lt; <code>bound</code>
694          * and <code>Character.isWhitespace(fDocument.getChar(pos))</code>
695          * evaluates to <code>false</code> and the position is in the default
696          * partition.
697          * 
698          * @param position
699          *            the first character position in <code>fDocument</code> to be
700          *            considered
701          * @param bound
702          *            the first position in <code>fDocument</code> to not consider
703          *            any more, with <code>bound</code> &gt; <code>position</code>,
704          *            or <code>UNBOUND</code>
705          * @return the smallest position of a non-whitespace character in [<code>position</code>,
706          *         <code>bound</code>) that resides in a Java partition, or
707          *         <code>NOT_FOUND</code> if none can be found
708          */
709         public int findNonWhitespaceForward(int position, int bound) {
710                 return scanForward(position, bound, fNonWSDefaultPart);
711         }
712
713         /**
714          * Finds the smallest position in <code>fDocument</code> such that the
715          * position is &gt;= <code>position</code> and &lt; <code>bound</code>
716          * and <code>Character.isWhitespace(fDocument.getChar(pos))</code>
717          * evaluates to <code>false</code>.
718          * 
719          * @param position
720          *            the first character position in <code>fDocument</code> to be
721          *            considered
722          * @param bound
723          *            the first position in <code>fDocument</code> to not consider
724          *            any more, with <code>bound</code> &gt; <code>position</code>,
725          *            or <code>UNBOUND</code>
726          * @return the smallest position of a non-whitespace character in [<code>position</code>,
727          *         <code>bound</code>), or <code>NOT_FOUND</code> if none can
728          *         be found
729          */
730         public int findNonWhitespaceForwardInAnyPartition(int position, int bound) {
731                 return scanForward(position, bound, fNonWS);
732         }
733
734         /**
735          * Finds the highest position in <code>fDocument</code> such that the
736          * position is &lt;= <code>position</code> and &gt; <code>bound</code>
737          * and <code>Character.isWhitespace(fDocument.getChar(pos))</code>
738          * evaluates to <code>false</code> and the position is in the default
739          * partition.
740          * 
741          * @param position
742          *            the first character position in <code>fDocument</code> to be
743          *            considered
744          * @param bound
745          *            the first position in <code>fDocument</code> to not consider
746          *            any more, with <code>bound</code> &lt; <code>position</code>,
747          *            or <code>UNBOUND</code>
748          * @return the highest position of a non-whitespace character in (<code>bound</code>,
749          *         <code>position</code>] that resides in a Java partition, or
750          *         <code>NOT_FOUND</code> if none can be found
751          */
752         public int findNonWhitespaceBackward(int position, int bound) {
753                 return scanBackward(position, bound, fNonWSDefaultPart);
754         }
755
756         /**
757          * Finds the lowest position <code>p</code> in <code>fDocument</code>
758          * such that <code>start</code> &lt;= p &lt; <code>bound</code> and
759          * <code>condition.stop(fDocument.getChar(p), p)</code> evaluates to
760          * <code>true</code>.
761          * 
762          * @param start
763          *            the first character position in <code>fDocument</code> to be
764          *            considered
765          * @param bound
766          *            the first position in <code>fDocument</code> to not consider
767          *            any more, with <code>bound</code> &gt; <code>start</code>,
768          *            or <code>UNBOUND</code>
769          * @param condition
770          *            the <code>StopCondition</code> to check
771          * @return the lowest position in [<code>start</code>,
772          *         <code>bound</code>) for which <code>condition</code> holds,
773          *         or <code>NOT_FOUND</code> if none can be found
774          */
775         public int scanForward(int start, int bound, StopCondition condition) {
776                 Assert.isTrue(start >= 0);
777
778                 if (bound == UNBOUND)
779                         bound = fDocument.getLength();
780
781                 Assert.isTrue(bound <= fDocument.getLength());
782
783                 try {
784                         fPos = start;
785                         while (fPos < bound) {
786
787                                 fChar = fDocument.getChar(fPos);
788                                 // omit closing tag
789                                 if (fChar == '?') {
790                                         if (fPos < fDocument.getLength() - 1) {
791                                                 if (fDocument.get(fPos - 1, 2).equalsIgnoreCase("?>")) {
792                                                         fPos++;
793                                                         return NOT_FOUND;
794                                                 }
795                                         }
796                                 }
797                                 if (condition.stop(fChar, fPos, true))
798                                         return fPos;
799
800                                 fPos++;
801                         }
802                 } catch (BadLocationException e) {
803                 }
804                 return NOT_FOUND;
805         }
806
807         /**
808          * Finds the lowest position in <code>fDocument</code> such that the
809          * position is &gt;= <code>position</code> and &lt; <code>bound</code>
810          * and <code>fDocument.getChar(position) == ch</code> evaluates to
811          * <code>true</code> and the position is in the default partition.
812          * 
813          * @param position
814          *            the first character position in <code>fDocument</code> to be
815          *            considered
816          * @param bound
817          *            the first position in <code>fDocument</code> to not consider
818          *            any more, with <code>bound</code> &gt; <code>position</code>,
819          *            or <code>UNBOUND</code>
820          * @param ch
821          *            the <code>char</code> to search for
822          * @return the lowest position of <code>ch</code> in (<code>bound</code>,
823          *         <code>position</code>] that resides in a Java partition, or
824          *         <code>NOT_FOUND</code> if none can be found
825          */
826         public int scanForward(int position, int bound, char ch) {
827                 return scanForward(position, bound, new CharacterMatch(ch));
828         }
829
830         /**
831          * Finds the lowest position in <code>fDocument</code> such that the
832          * position is &gt;= <code>position</code> and &lt; <code>bound</code>
833          * and <code>fDocument.getChar(position) == ch</code> evaluates to
834          * <code>true</code> for at least one ch in <code>chars</code> and the
835          * position is in the default partition.
836          * 
837          * @param position
838          *            the first character position in <code>fDocument</code> to be
839          *            considered
840          * @param bound
841          *            the first position in <code>fDocument</code> to not consider
842          *            any more, with <code>bound</code> &gt; <code>position</code>,
843          *            or <code>UNBOUND</code>
844          * @param chars
845          *            an array of <code>char</code> to search for
846          * @return the lowest position of a non-whitespace character in [<code>position</code>,
847          *         <code>bound</code>) that resides in a Java partition, or
848          *         <code>NOT_FOUND</code> if none can be found
849          */
850         public int scanForward(int position, int bound, char[] chars) {
851                 return scanForward(position, bound, new CharacterMatch(chars));
852         }
853
854         /**
855          * Finds the highest position <code>p</code> in <code>fDocument</code>
856          * such that <code>bound</code> &lt; <code>p</code> &lt;=
857          * <code>start</code> and
858          * <code>condition.stop(fDocument.getChar(p), p)</code> evaluates to
859          * <code>true</code>.
860          * 
861          * @param start
862          *            the first character position in <code>fDocument</code> to be
863          *            considered
864          * @param bound
865          *            the first position in <code>fDocument</code> to not consider
866          *            any more, with <code>bound</code> &lt; <code>start</code>,
867          *            or <code>UNBOUND</code>
868          * @param condition
869          *            the <code>StopCondition</code> to check
870          * @return the highest position in (<code>bound</code>,
871          *         <code>start</code> for which <code>condition</code> holds, or
872          *         <code>NOT_FOUND</code> if none can be found
873          */
874         public int scanBackward(int start, int bound, StopCondition condition) {
875                 if (bound == UNBOUND)
876                         bound = -1;
877
878                 Assert.isTrue(bound >= -1);
879                 Assert.isTrue(start < fDocument.getLength());
880
881                 try {
882                         fPos = start;
883                         while (fPos > bound) {
884
885                                 fChar = fDocument.getChar(fPos);
886                                 // omit opening tag
887                                 if (fChar == 'p' || fChar == 'P') {
888                                         if (fPos >= 4) {
889                                                 if (fDocument.get(fPos - 4, 5).equalsIgnoreCase("<?php")) {
890                                                         fPos -= 4;
891                                                         return NOT_FOUND;
892                                                 }
893                                         }
894                                 } else if (fChar == '?') {
895                                         if (fPos >= 1) {
896                                                 if (fDocument.get(fPos - 1, 2).equalsIgnoreCase("<?")) {
897                                                         fPos--;
898                                                         return NOT_FOUND;
899                                                 }
900                                         }
901                                 }
902                                 if (condition.stop(fChar, fPos, false))
903                                         return fPos;
904
905                                 fPos--;
906                         }
907                 } catch (BadLocationException e) {
908                 }
909                 return NOT_FOUND;
910         }
911
912         /**
913          * Finds the highest position in <code>fDocument</code> such that the
914          * position is &lt;= <code>position</code> and &gt; <code>bound</code>
915          * and <code>fDocument.getChar(position) == ch</code> evaluates to
916          * <code>true</code> for at least one ch in <code>chars</code> and the
917          * position is in the default partition.
918          * 
919          * @param position
920          *            the first character position in <code>fDocument</code> to be
921          *            considered
922          * @param bound
923          *            the first position in <code>fDocument</code> to not consider
924          *            any more, with <code>bound</code> &lt; <code>position</code>,
925          *            or <code>UNBOUND</code>
926          * @param ch
927          *            the <code>char</code> to search for
928          * @return the highest position of one element in <code>chars</code> in (<code>bound</code>,
929          *         <code>position</code>] that resides in a Java partition, or
930          *         <code>NOT_FOUND</code> if none can be found
931          */
932         public int scanBackward(int position, int bound, char ch) {
933                 return scanBackward(position, bound, new CharacterMatch(ch));
934         }
935
936         /**
937          * Finds the highest position in <code>fDocument</code> such that the
938          * position is &lt;= <code>position</code> and &gt; <code>bound</code>
939          * and <code>fDocument.getChar(position) == ch</code> evaluates to
940          * <code>true</code> for at least one ch in <code>chars</code> and the
941          * position is in the default partition.
942          * 
943          * @param position
944          *            the first character position in <code>fDocument</code> to be
945          *            considered
946          * @param bound
947          *            the first position in <code>fDocument</code> to not consider
948          *            any more, with <code>bound</code> &lt; <code>position</code>,
949          *            or <code>UNBOUND</code>
950          * @param chars
951          *            an array of <code>char</code> to search for
952          * @return the highest position of one element in <code>chars</code> in (<code>bound</code>,
953          *         <code>position</code>] that resides in a Java partition, or
954          *         <code>NOT_FOUND</code> if none can be found
955          */
956         public int scanBackward(int position, int bound, char[] chars) {
957                 return scanBackward(position, bound, new CharacterMatch(chars));
958         }
959
960         /**
961          * Checks whether <code>position</code> resides in a default (Java)
962          * partition of <code>fDocument</code>.
963          * 
964          * @param position
965          *            the position to be checked
966          * @return <code>true</code> if <code>position</code> is in the default
967          *         partition of <code>fDocument</code>, <code>false</code>
968          *         otherwise
969          */
970         public boolean isDefaultPartition(int position) {
971                 Assert.isTrue(position >= 0);
972                 Assert.isTrue(position <= fDocument.getLength());
973
974                 try {
975                         ITypedRegion region = TextUtilities.getPartition(fDocument,
976                                         fPartitioning, position, false);
977                         return region.getType().equals(fPartition);
978
979                 } catch (BadLocationException e) {
980                 }
981
982                 return false;
983         }
984
985         /**
986          * Checks if the line seems to be an open condition not followed by a block
987          * (i.e. an if, while, or for statement with just one following statement,
988          * see example below).
989          * 
990          * <pre>
991          * if (condition)
992          *      doStuff();
993          * </pre>
994          * 
995          * <p>
996          * Algorithm: if the last non-WS, non-Comment code on the line is an if
997          * (condition), while (condition), for( expression), do, else, and there is
998          * no statement after that
999          * </p>
1000          * 
1001          * @param position
1002          *            the insert position of the new character
1003          * @param bound
1004          *            the lowest position to consider
1005          * @return <code>true</code> if the code is a conditional statement or
1006          *         loop without a block, <code>false</code> otherwise
1007          */
1008         public boolean isBracelessBlockStart(int position, int bound) {
1009                 if (position < 1)
1010                         return false;
1011
1012                 switch (previousToken(position, bound)) {
1013                 case TokenDO:
1014                 case TokenELSE:
1015                         return true;
1016                 case TokenRPAREN:
1017                         position = findOpeningPeer(fPos, LPAREN, RPAREN);
1018                         if (position > 0) {
1019                                 switch (previousToken(position - 1, bound)) {
1020                                 case TokenIF:
1021                                 case TokenFOR:
1022                                 case TokenWHILE:
1023                                         return true;
1024                                 }
1025                         }
1026                 }
1027
1028                 return false;
1029         }
1030 }