improved syntax highlighting scanners and preferences
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpeclipse / phpeditor / php / PHPPartitionScanner.java
1 /**
2  * This program and the accompanying materials
3  * are made available under the terms of the Common Public License v1.0
4  * which accompanies this distribution, and is available at
5  * http://www.eclipse.org/legal/cpl-v10.html
6  * Created on 05.03.2003
7  *
8  * @author Stefan Langer (musk)
9  * @version $Revision: 1.24 $
10  */
11 package net.sourceforge.phpeclipse.phpeditor.php;
12
13 import java.util.ArrayList;
14 import java.util.HashMap;
15 import java.util.Map;
16
17 import net.sourceforge.phpdt.internal.ui.text.*;
18
19 import org.eclipse.jface.text.Assert;
20 import org.eclipse.jface.text.BadLocationException;
21 import org.eclipse.jface.text.IDocument;
22 import org.eclipse.jface.text.ITypedRegion;
23 import org.eclipse.jface.text.rules.ICharacterScanner;
24 import org.eclipse.jface.text.rules.IPartitionTokenScanner;
25 import org.eclipse.jface.text.rules.IToken;
26 import org.eclipse.jface.text.rules.Token;
27
28 /**
29  * 
30  */
31 public class PHPPartitionScanner implements IPartitionTokenScanner {
32   private static final boolean DEBUG = false;
33
34   private boolean fInString = false;
35   private boolean fInDoubString = false;
36   private IDocument fDocument = null;
37   private int fOffset = -1;
38   private String fContentType = IPHPPartitions.HTML;
39   private String fPrevContentType = IPHPPartitions.HTML;
40   private boolean partitionBorder = false;
41   private int fTokenOffset;
42   private int fEnd = -1;
43   private int fLength;
44   private int fCurrentLength;
45   private int fFileType;
46   private Map tokens = new HashMap();
47
48   public PHPPartitionScanner() {
49         this(IPHPPartitions.PHP_FILE);
50   }
51   
52   public PHPPartitionScanner(int fileType) {
53     this.tokens.put(IPHPPartitions.PHP_PARTITIONING, new Token(IPHPPartitions.PHP_PARTITIONING));
54     this.tokens.put(IPHPPartitions.PHP_STRING_DQ, new Token(IPHPPartitions.PHP_STRING_DQ));
55     this.tokens.put(
56       IPHPPartitions.PHP_PHPDOC_COMMENT,
57       new Token(IPHPPartitions.PHP_PHPDOC_COMMENT));
58     this.tokens.put(IPHPPartitions.HTML, new Token(IPHPPartitions.HTML));
59     this.tokens.put(
60       IPHPPartitions.HTML_MULTILINE_COMMENT,
61       new Token(IPHPPartitions.HTML_MULTILINE_COMMENT));
62
63     this.tokens.put(IPHPPartitions.SMARTY, new Token(IPHPPartitions.SMARTY));
64     this.tokens.put(
65       IPHPPartitions.SMARTY_MULTILINE_COMMENT,
66       new Token(IPHPPartitions.SMARTY_MULTILINE_COMMENT));
67
68     this.tokens.put(IDocument.DEFAULT_CONTENT_TYPE, new Token(IDocument.DEFAULT_CONTENT_TYPE));
69     fFileType = fileType;
70   }
71
72   private IToken getToken(String type) {
73     fLength = fCurrentLength;
74     if (DEBUG) {
75
76       try {
77         if (fLength <= 0) {
78           int line = fDocument.getLineOfOffset(fOffset);
79           System.err.println("Error at " + line + " offset:" + String.valueOf(fOffset - fDocument.getLineOffset(line)));
80         }
81       } catch (BadLocationException e) { // should never happen
82         // TODO Write stacktrace to log
83         e.printStackTrace();
84       }
85     }
86     Assert.isTrue(fLength > 0, "Partition length <= 0!");
87     fCurrentLength = 0;
88     // String can never cross partition borders so reset string detection
89     fInString = false;
90     fInDoubString = false;
91     IToken token = (IToken) this.tokens.get(type);
92     Assert.isNotNull(token, "Token for type \"" + type + "\" not found!");
93     if (DEBUG) {
94       System.out.println("Partition: fTokenOffset=" + fTokenOffset + " fContentType=" + type + " fLength=" + fLength);
95     }
96     return token;
97   }
98
99   /* (non-Javadoc)
100    * @see org.eclipse.jface.text.rules.IPartitionTokenScanner#setPartialRange(org.eclipse.jface.text.IDocument, int, int, java.lang.String, int)
101    */
102   public void setPartialRange(IDocument document, int offset, int length, String contentType, int partitionOffset) {
103     if (DEBUG) {
104       System.out.println("*****");
105       System.out.println("PartialRange: contentType=" + contentType + " partitionOffset=" + partitionOffset);
106     }
107
108     try {
109       if (partitionOffset > -1) {
110         partitionBorder = false;
111         // because of strings we have to parse the whole partition
112         this.setRange(document, partitionOffset, offset - partitionOffset + length);
113         // sometimes we get a wrong partition so we retrieve the partition
114         // directly from the document
115         fContentType = fDocument.getContentType(partitionOffset);
116       } else
117         this.setRange(document, offset, length);
118
119     } catch (BadLocationException e) {
120       // should never happen
121       // TODO print stack trace to log
122       // fall back just scan the whole document again
123       this.setRange(document, 0, fDocument.getLength());
124     }
125
126   }
127
128   /* (non-Javadoc)
129    * @see org.eclipse.jface.text.rules.ITokenScanner#getTokenLength()
130    */
131   public int getTokenLength() {
132     return fLength;
133   }
134
135   /* (non-Javadoc)
136    * @see org.eclipse.jface.text.rules.ITokenScanner#getTokenOffset()
137    */
138   public int getTokenOffset() {
139     return fTokenOffset;
140   }
141
142   /* (non-Javadoc)
143    * @see org.eclipse.jface.text.rules.ITokenScanner#nextToken()
144    */
145   public IToken nextToken() {
146     int c;
147
148     // check if we are not allready at the end of the
149     // file
150     if ((c = read()) == ICharacterScanner.EOF) {
151       partitionBorder = false;
152       return Token.EOF;
153     } else
154       unread();
155
156     if (partitionBorder) {
157       fTokenOffset = fOffset;
158       partitionBorder = false;
159     }
160
161     while ((c = read()) != ICharacterScanner.EOF) {
162       switch (c) {
163         case '<' :
164           if (!isInString(IPHPPartitions.PHP_PARTITIONING)
165             && fContentType != IPHPPartitions.PHP_PHPDOC_COMMENT
166             && checkPattern(new char[] { '?', 'p', 'h', 'p' }, true)) {
167             if (fContentType != IPHPPartitions.PHP_PARTITIONING && fCurrentLength > 5) {
168               unread(5);
169               IToken token = getToken(fContentType);
170               // save previouse contenttype
171               //TODO build stack for previouse contenttype 
172               fPrevContentType = fContentType;
173
174               fContentType = IPHPPartitions.PHP_PARTITIONING;
175
176               return token;
177             } else
178               fContentType = IPHPPartitions.PHP_PARTITIONING;
179
180             // remember offset of this partition
181             fTokenOffset = fOffset - 5;
182             fCurrentLength = 5;
183           } else if (
184             !isInString(IPHPPartitions.PHP_PARTITIONING)
185               && fContentType != IPHPPartitions.PHP_PHPDOC_COMMENT
186               && checkPattern(new char[] { '?' }, false)) {
187             if (fContentType != IPHPPartitions.PHP_PARTITIONING && fCurrentLength > 2) {
188               unread(2);
189               IToken token = getToken(fContentType);
190               // save previouse contenttype
191               fPrevContentType = fContentType;
192               fContentType = IPHPPartitions.PHP_PARTITIONING;
193               return token;
194             } else
195               fContentType = IPHPPartitions.PHP_PARTITIONING;
196             // remember offset of this partition
197             fTokenOffset = fOffset - 2;
198             fCurrentLength = 2;
199           } else if (
200             !isInString(IPHPPartitions.PHP_PARTITIONING)
201               && (fContentType != IPHPPartitions.PHP_PARTITIONING) // BUG #769044
202               && (fContentType != IPHPPartitions.PHP_PHPDOC_COMMENT) // BUG #769044
203               && checkPattern(new char[] { '!', '-', '-' })) { // return previouse partition
204             if (fContentType != IPHPPartitions.HTML_MULTILINE_COMMENT && fCurrentLength > 4) {
205               unread(4);
206               IToken token = getToken(fContentType);
207               fContentType = IPHPPartitions.HTML_MULTILINE_COMMENT;
208               return token;
209             } else
210               fContentType = IPHPPartitions.HTML_MULTILINE_COMMENT;
211
212             fTokenOffset = fOffset - 4;
213             fCurrentLength = 4;
214           }
215           break;
216         case '?' :
217           if (!isInString(IPHPPartitions.PHP_PARTITIONING) && fContentType == IPHPPartitions.PHP_PARTITIONING) {
218             if ((c = read()) == '>') {
219               if (fPrevContentType != null)
220                 fContentType = fPrevContentType;
221               else
222                 fContentType = IPHPPartitions.HTML;
223               partitionBorder = true;
224               return getToken(IPHPPartitions.PHP_PARTITIONING);
225             } else if (c != ICharacterScanner.EOF)
226               unread();
227           }
228           break;
229         case '-' :
230           if (!isInString(IPHPPartitions.PHP_PARTITIONING)
231             && fContentType == IPHPPartitions.HTML_MULTILINE_COMMENT
232             && checkPattern(new char[] { '-', '>' })) {
233             fContentType = IPHPPartitions.HTML;
234             partitionBorder = true;
235             return getToken(IPHPPartitions.HTML_MULTILINE_COMMENT);
236           }
237           break;
238         case '{' : // SMARTY code starts here ?
239           if (fFileType == IPHPPartitions.SMARTY_FILE) {
240             if ((c = read()) == '*') {
241               if (DEBUG) {
242                 System.out.println(
243                   "SMARTYDOC_TOKEN start "
244                     + fTokenOffset
245                     + " fContentType="
246                     + fContentType
247                     + " fLength="
248                     + fLength
249                     + " fOffset="
250                     + fOffset
251                     + " fCurrentLength="
252                     + fCurrentLength);
253               }
254               if (fContentType != IPHPPartitions.SMARTY_MULTILINE_COMMENT && fCurrentLength > 2) {
255                 // SMARTY doc code starts here 
256                 unread(2);
257                 IToken token = getToken(fContentType);
258                 fContentType = IPHPPartitions.SMARTY_MULTILINE_COMMENT;
259                 return token;
260                 //              } else if (fContentType == IPHPPartitionScannerConstants.HTML && fOffset == 2) {
261                 //                fContentType = IPHPPartitionScannerConstants.SMARTY_MULTILINE_COMMENT;
262               } else { // if (fContentType == IPHPPartitionScannerConstants.SMARTY_MULTILINE_COMMENT) {
263                 fContentType = IPHPPartitions.SMARTY_MULTILINE_COMMENT;
264                 fTokenOffset = fOffset - 2;
265                 fCurrentLength = 2;
266               }
267               break;
268             }
269             if (DEBUG) {
270               System.out.println(
271                 "SMARTY_TOKEN start "
272                   + fTokenOffset
273                   + " fContentType="
274                   + fContentType
275                   + " fLength="
276                   + fLength
277                   + " fOffset="
278                   + fOffset);
279             }
280             if (c != ICharacterScanner.EOF) {
281                 unread();
282             }
283             if (fContentType != IPHPPartitions.SMARTY && fCurrentLength > 1) {
284               unread(1);
285               IToken token = getToken(fContentType);
286               fContentType = IPHPPartitions.SMARTY;
287               return token;
288               //            } else if (fContentType == IPHPPartitionScannerConstants.HTML && fOffset==1) {
289               //              fContentType = IPHPPartitionScannerConstants.SMARTY;
290             } else {
291               fContentType = IPHPPartitions.SMARTY;
292               fTokenOffset = fOffset - 1;
293               fCurrentLength = 1;
294             }
295           }
296           break;
297         case '}' : // SMARTY code ends here ?
298           if (fFileType == IPHPPartitions.SMARTY_FILE && fContentType == IPHPPartitions.SMARTY) {
299             if (DEBUG) {
300               System.out.println(
301                 "SMARTY_TOKEN end "
302                   + fTokenOffset
303                   + " fContentType="
304                   + fContentType
305                   + " fLength="
306                   + fLength
307                   + " fOffset="
308                   + fOffset);
309             }
310             fContentType = IPHPPartitions.HTML;
311             partitionBorder = true;
312             return getToken(IPHPPartitions.SMARTY);
313           }
314           break;
315         case '/' :
316           if (!isInString(IPHPPartitions.PHP_PARTITIONING) && (c = read()) == '*') { // MULTINE COMMENT JAVASCRIPT, CSS, PHP
317             if (fContentType == IPHPPartitions.PHP_PARTITIONING && fCurrentLength > 2) {
318               unread(2);
319               IToken token = getToken(fContentType);
320               fContentType = IPHPPartitions.PHP_PHPDOC_COMMENT;
321               return token;
322             } else if (fContentType == IPHPPartitions.PHP_PHPDOC_COMMENT) {
323               fTokenOffset = fOffset - 2;
324               fCurrentLength = 2;
325             }
326
327           } else if (!isInString(IPHPPartitions.PHP_PARTITIONING) && c != ICharacterScanner.EOF)
328             unread();
329           break;
330         case '*' :
331           if (!isInString(IPHPPartitions.PHP_PARTITIONING) && (c = read()) == '/') {
332             if (fContentType == IPHPPartitions.PHP_PHPDOC_COMMENT) {
333               fContentType = IPHPPartitions.PHP_PARTITIONING;
334               partitionBorder = true;
335               return getToken(IPHPPartitions.PHP_PHPDOC_COMMENT);
336             } else if (fContentType == IPHPPartitions.CSS_MULTILINE_COMMENT) {
337             } else if (fContentType == IPHPPartitions.JS_MULTILINE_COMMENT) {
338             }
339           } else if (fFileType == IPHPPartitions.SMARTY_FILE && (c = read()) == '}') {
340             if (DEBUG) {
341               System.out.println(
342                 "SMARTYDOC_TOKEN end "
343                   + fTokenOffset
344                   + " fContentType="
345                   + fContentType
346                   + " fLength="
347                   + fLength
348                   + " fOffset="
349                   + fOffset);
350             }
351             if (fContentType == IPHPPartitions.SMARTY_MULTILINE_COMMENT) {
352               fContentType = IPHPPartitions.HTML;
353               partitionBorder = true;
354               return getToken(IPHPPartitions.SMARTY_MULTILINE_COMMENT);
355             }
356           } else if (!isInString(IPHPPartitions.PHP_PARTITIONING) && c != ICharacterScanner.EOF) {
357             unread();
358           }
359           break;
360         case '\'' :
361           if (!fInDoubString)
362             fInString = !fInString;
363           break;
364         case '"' :
365           // toggle String mode
366           if (!fInString)
367             fInDoubString = !fInDoubString;
368           break;
369       }
370     } // end of file reached but we have to return the
371     // last partition.
372     return getToken(fContentType);
373   }
374   /* (non-Javadoc)
375    * @see org.eclipse.jface.text.rules.ITokenScanner#setRange(org.eclipse.jface.text.IDocument, int, int)
376    */
377   public void setRange(IDocument document, int offset, int length) {
378     if (DEBUG) {
379       System.out.println("SET RANGE: offset=" + offset + " length=" + length);
380     }
381
382     fDocument = document;
383     fOffset = offset;
384     fTokenOffset = offset;
385     fCurrentLength = 0;
386     fLength = 0;
387     fEnd = fOffset + length;
388     fInString = false;
389     fInDoubString = false;
390     fContentType = IPHPPartitions.HTML;
391     //        String[] prev = getPartitionStack(offset);
392   }
393
394   private int read() {
395     try {
396       if (fOffset < fEnd) {
397         fCurrentLength++;
398         return fDocument.getChar(fOffset++);
399       }
400       return ICharacterScanner.EOF;
401     } catch (BadLocationException e) {
402       // should never happen
403       // TODO write stacktrace to log
404       fOffset = fEnd;
405       return ICharacterScanner.EOF;
406     }
407   }
408
409   private void unread() {
410     --fOffset;
411     --fCurrentLength;
412   }
413
414   private void unread(int num) {
415     fOffset -= num;
416     fCurrentLength -= num;
417   }
418
419   private boolean checkPattern(char[] pattern) {
420     return checkPattern(pattern, false);
421   }
422
423   /**
424    * Check if next character sequence read from document is equals to 
425    * the provided pattern. Pattern is read from left to right until the 
426    * first character read doesn't match. If this happens all read characters are
427    * unread.
428    * @param pattern The pattern to check.
429    * @return <code>true</code> if pattern is equals else returns <code>false</code>.
430    */
431   private boolean checkPattern(char[] pattern, boolean ignoreCase) {
432     int prevOffset = fOffset;
433     int prevLength = fCurrentLength;
434     for (int i = 0; i < pattern.length; i++) {
435       int c = read();
436
437       if (c == ICharacterScanner.EOF || !letterEquals(c, pattern[i], ignoreCase)) {
438         fOffset = prevOffset;
439         fCurrentLength = prevLength;
440         return false;
441       }
442     }
443
444     return true;
445   }
446
447   private boolean letterEquals(int test, char letter, boolean ignoreCase) {
448     if (test == letter)
449       return true;
450     else if (ignoreCase && Character.isLowerCase(letter) && test == Character.toUpperCase(letter))
451       return true;
452     else if (ignoreCase && Character.isUpperCase(letter) && test == Character.toLowerCase(letter))
453       return true;
454
455     return false;
456   }
457
458   /**
459    * Checks wether the offset is in a <code>String</code> and the specified 
460    * contenttype is the current content type.
461    * Strings are delimited, mutual exclusive, by a " or by a '.
462    * 
463    * @param contentType The contenttype to check.
464    * @return <code>true</code> if the current offset is in a string else 
465    *                    returns false.
466    */
467   private boolean isInString(String contentType) {
468     if (fContentType == contentType)
469       return (fInString || fInDoubString);
470     else
471       return false;
472   }
473
474   /**
475    * Returns the previouse partition stack for the given offset.
476    * 
477    * @param offset The offset to return the previouse partitionstack for.
478    * 
479    * @return The stack as a string array.
480    */
481   private String[] getPartitionStack(int offset) {
482     ArrayList types = new ArrayList();
483     int tmpOffset = 0;
484     try {
485       ITypedRegion region = fDocument.getPartition(offset);
486       tmpOffset = region.getOffset();
487       while (tmpOffset - 1 > 0) {
488         region = fDocument.getPartition(tmpOffset - 1);
489         tmpOffset = region.getOffset();
490         types.add(0, region.getType());
491       }
492     } catch (BadLocationException e) {
493       if (DEBUG) {
494         e.printStackTrace();
495       }
496     }
497
498     String[] retVal = new String[types.size()];
499
500     retVal = (String[]) types.toArray(retVal);
501     return retVal;
502   }
503
504 }