Fix #1368081: First suggestion in #1368081 seems to be working better
[phpeclipse.git] / net.sourceforge.phpeclipse.ui / src / net / sourceforge / phpeclipse / ui / text / rules / AbstractPartitioner.java
1 /*
2  * Copyright (c) 2002-2004 Widespace, OU 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://solareclipse.sourceforge.net/legal/cpl-v10.html
7  * 
8  * Contributors:
9  *     Igor Malinin - initial contribution
10  * 
11  * $Id: AbstractPartitioner.java,v 1.3 2004-12-29 21:42:31 axelcl Exp $
12  */
13
14 package net.sourceforge.phpeclipse.ui.text.rules;
15
16 import java.util.ArrayList;
17 import java.util.List;
18
19 import org.eclipse.jface.text.Assert;
20 import org.eclipse.jface.text.DocumentEvent;
21 import org.eclipse.jface.text.IDocument;
22 import org.eclipse.jface.text.IDocumentPartitioner;
23 import org.eclipse.jface.text.IDocumentPartitionerExtension;
24 import org.eclipse.jface.text.IRegion;
25 import org.eclipse.jface.text.ITypedRegion;
26 import org.eclipse.jface.text.Region;
27 import org.eclipse.jface.text.TypedRegion;
28 import org.eclipse.jface.text.rules.IPartitionTokenScanner;
29 import org.eclipse.jface.text.rules.IToken;
30
31 /**
32  * Advanced partitioner which maintains partitions as views to connected document. Views have own partitioners themselves. This
33  * class is designed as a base for complex partitioners such as for JSP, PHP, ASP, etc. languages.
34  * 
35  * @author Igor Malinin
36  */
37 public abstract class AbstractPartitioner implements IDocumentPartitioner, IDocumentPartitionerExtension {
38   public final static boolean DEBUG = false;
39
40   /** Partition scanner */
41   protected IPartitionTokenScanner scanner;
42
43   /** Connected document */
44   protected IDocument document;
45
46   /** Flat structure of the document */
47   protected List nodes = new ArrayList();
48
49   /** The offset at which the first changed partition starts */
50   protected int regionStart;
51
52   /** The offset at which the last changed partition ends */
53   protected int regionEnd;
54
55   public AbstractPartitioner(IPartitionTokenScanner scanner) {
56     this.scanner = scanner;
57   }
58
59   protected FlatNode createNode(String type, int offset, int length) {
60     if (DEBUG) {
61       Assert.isTrue(offset >= 0, Integer.toString(offset));
62     }
63     FlatNode node = new FlatNode(type);
64     node.offset = offset;
65     node.length = length;
66     return node;
67   }
68
69   protected void addInnerRegion(FlatNode position) {
70     nodes.add(computeFlatNodeIndex(position.offset), position);
71   }
72
73   protected void removeInnerRegion(FlatNode position) {
74     nodes.remove(position); // TODO: Indexed remove?
75   }
76
77   protected void deleteInnerRegion(FlatNode position) {
78     nodes.remove(position); // TODO: Indexed remove?
79   }
80
81   protected void resizeInnerRegion(FlatNode position) {
82   }
83
84   /*
85    * @see org.eclipse.jface.text.IDocumentPartitioner#connect(IDocument)
86    */
87   public void connect(IDocument document) {
88     this.document = document;
89
90     initialize();
91   }
92
93   /*
94    * @see org.eclipse.jface.text.IDocumentPartitioner#disconnect()
95    */
96   public void disconnect() {
97     nodes.clear();
98     document = null;
99   }
100
101   /**
102    * Performs the initial partitioning of the partitioner's document.
103    */
104   protected void initialize() {
105     scanner.setRange(document, 0, document.getLength());
106     // axelcl start
107     nodes.clear();
108     // axelcl end
109     IToken token = scanner.nextToken();
110     while (!token.isEOF()) {
111       String contentType = getTokenContentType(token);
112
113       if (isSupportedContentType(contentType)) {
114         addInnerRegion(createNode(contentType, scanner.getTokenOffset(), scanner.getTokenLength()));
115       }
116
117       token = scanner.nextToken();
118     }
119   }
120
121   /*
122    * @see org.eclipse.jface.text.IDocumentPartitioner#documentAboutToBeChanged(DocumentEvent)
123    */
124   public void documentAboutToBeChanged(DocumentEvent event) {
125     regionStart = regionEnd = -1;
126   }
127
128   /*
129    * @see org.eclipse.jface.text.IDocumentPartitioner#documentChanged(DocumentEvent)
130    */
131   public boolean documentChanged(DocumentEvent event) {
132     return (documentChanged2(event) != null);
133   }
134
135   /*
136    * @see org.eclipse.jface.text.IDocumentPartitionerExtension#documentChanged2(DocumentEvent)
137    */
138   public IRegion documentChanged2(DocumentEvent event) {
139     int first = fixupPartitions(event);
140
141     FlatNode[] category = (FlatNode[]) nodes.toArray(new FlatNode[nodes.size()]);
142
143     // repartition changed region
144
145     String contentType = IDocument.DEFAULT_CONTENT_TYPE;
146
147     int offset;
148
149     if (first == 0) {
150       offset = 0; // Bug #697414: first offset
151     } else {
152       offset = event.getOffset();
153
154       FlatNode partition = category[first - 1];
155       if (partition.includes(offset)) {
156         offset = partition.offset;
157         contentType = partition.type;
158         --first;
159       } else if (offset == partition.offset + partition.length) {
160         offset = partition.offset;
161         contentType = partition.type;
162         --first;
163       } else {
164         offset = partition.offset + partition.length;
165       }
166     }
167
168     // should not be changed since last conversion
169     // category = (FlatNode[]) nodes.toArray(new FlatNode[nodes.size()]);
170     if (DEBUG) {
171       Assert.isTrue(offset >= 0, Integer.toString(offset));
172     }
173     scanner.setPartialRange(document, offset, document.getLength(), contentType, offset);
174
175     int lastScannedPosition = offset;
176     IToken token = scanner.nextToken();
177     while (!token.isEOF()) {
178       contentType = getTokenContentType(token);
179
180       if (!isSupportedContentType(contentType)) {
181         token = scanner.nextToken();
182         continue;
183       }
184
185       offset = scanner.getTokenOffset();
186       if (DEBUG) {
187         Assert.isTrue(offset >= 0, scanner.toString());
188       }
189       int length = scanner.getTokenLength();
190
191       lastScannedPosition = offset + length;
192
193       // remove all affected positions
194       while (first < category.length) {
195         FlatNode p = category[first];
196         if (p.offset + p.length < lastScannedPosition
197             || (p.overlapsWith(offset, length) && (!containsPosition(offset, length) || !contentType.equals(p.type)))) {
198           removeInnerRegion(p);
199           rememberRegion(p.offset, p.length);
200           ++first;
201         } else {
202           break;
203         }
204       }
205
206       // if position already exists we are done
207       if (containsPosition(offset, length)) {
208         if (lastScannedPosition > event.getOffset()) {
209           // TODO: optional repartition till end of doc
210           return createRegion();
211         }
212
213         ++first;
214       } else {
215         // insert the new type position
216         addInnerRegion(createNode(contentType, offset, length));
217         rememberRegion(offset, length);
218       }
219       //            try {
220       token = scanner.nextToken();
221       //            } catch (ArrayIndexOutOfBoundsException e) {
222       //              System.out.println(this.getClass().toString());
223       //              throw e;
224       //            }
225     }
226
227     // remove all positions behind lastScannedPosition
228     // since there aren't any further types
229
230     // Do not need to recalculate (lost remove events)!
231     // first = computeIndexInInnerDocuments(lastScannedPosition);
232     while (first < category.length) {
233       FlatNode p = category[first++];
234       removeInnerRegion(p);
235       rememberRegion(p.offset, p.length);
236     }
237
238     return createRegion();
239   }
240
241   protected int fixupPartitions(DocumentEvent event) {
242     int offset = event.getOffset();
243     int length = event.getLength();
244     int end = offset + length;
245
246     // fixup flat nodes laying on change boundaries
247
248     int first = computeFlatNodeIndex(offset);
249     if (first > 0) {
250       FlatNode p = (FlatNode) nodes.get(first - 1);
251
252       int right = p.offset + p.length;
253       if (offset < right) {
254         // change overlaps with partition
255         if (end < right) {
256           // cahnge completely inside partition
257           String text = event.getText();
258           p.length -= length;
259           if (text != null) {
260             p.length += text.length();
261           }
262         } else {
263           // cut partition at right
264           int cut = p.offset + p.length - offset;
265           p.length -= cut;
266         }
267       }
268     }
269
270     int last = computeFlatNodeIndex(end);
271     if (first < last) {
272       FlatNode p = (FlatNode) nodes.get(last - 1);
273
274       int right = p.offset + p.length;
275       if (end < right) {
276         // cut partition at left
277         int cut = end - p.offset;
278         p.length -= cut;
279         p.offset = offset;
280
281         String text = event.getText();
282         if (text != null) {
283           p.offset += text.length();
284         }
285
286         --last;
287       }
288     }
289
290     // fixup flat nodes laying afrer change
291
292     String text = event.getText();
293     if (text != null) {
294       length -= text.length();
295     }
296
297     for (int i = last, size = nodes.size(); i < size; i++) {
298       ((FlatNode) nodes.get(i)).offset -= length;
299     }
300
301     // delete flat nodes laying completely inside change boundaries
302
303     if (first < last) {
304       do {
305         deleteInnerRegion((FlatNode) nodes.get(--last));
306       } while (first < last);
307
308       rememberRegion(offset, 0);
309     }
310
311     return first;
312   }
313
314   /**
315    * Returns whether the given type is one of the legal content types.
316    * 
317    * @param contentType
318    *          the content type to check
319    * @return <code>true</code> if the content type is a legal content type
320    */
321   protected boolean isSupportedContentType(String contentType) {
322     /* TODO: implementation */
323     //          if (contentType != null) {
324     //                  for (int i= 0; i < fLegalContentTypes.length; i++) {
325     //                          if (fLegalContentTypes[i].equals(contentType)) {
326     //                                  return true;
327     //                          }
328     //                  }
329     //          }
330     //          return false;
331     return (contentType != null);
332   }
333
334   /**
335    * Returns a content type encoded in the given token. If the token's data is not <code>null</code> and a string it is assumed
336    * that it is the encoded content type.
337    * 
338    * @param token
339    *          the token whose content type is to be determined
340    * @return the token's content type
341    */
342   protected String getTokenContentType(IToken token) {
343     Object data = token.getData();
344     if (data instanceof String) {
345       return (String) data;
346     }
347
348     return null;
349   }
350
351   /*
352    * @see org.eclipse.jface.text.IDocumentPartitioner#getLegalContentTypes()
353    */
354   public String[] getLegalContentTypes() {
355     // TODO: implementation
356     return null;
357   }
358
359   /*
360    * @see org.eclipse.jface.text.IDocumentPartitioner#getContentType(int)
361    */
362   public String getContentType(int offset) {
363     return getPartition(offset).getType();
364   }
365
366   /*
367    * @see org.eclipse.jface.text.IDocumentPartitioner#getPartition(int)
368    */
369   public ITypedRegion getPartition(int offset) {
370     if (nodes.size() == 0) {
371       return new TypedRegion(0, document.getLength(), IDocument.DEFAULT_CONTENT_TYPE);
372     }
373
374     int index = computeFlatNodeIndex(offset);
375     if (index < nodes.size()) {
376       FlatNode next = (FlatNode) nodes.get(index);
377
378       if (offset == next.offset) {
379         return new TypedRegion(next.offset, next.length, next.type);
380       }
381
382       if (index == 0) {
383         return new TypedRegion(0, next.offset, IDocument.DEFAULT_CONTENT_TYPE);
384       }
385
386       FlatNode prev = (FlatNode) nodes.get(index - 1);
387
388       if (prev.includes(offset)) {
389         return new TypedRegion(prev.offset, prev.length, prev.type);
390       }
391
392       int end = prev.offset + prev.length;
393       return new TypedRegion(end, next.offset - end, IDocument.DEFAULT_CONTENT_TYPE);
394     }
395
396     FlatNode prev = (FlatNode) nodes.get(nodes.size() - 1);
397
398     if (prev.includes(offset)) {
399       return new TypedRegion(prev.offset, prev.length, prev.type);
400     }
401
402     int end = prev.offset + prev.length;
403
404     return new TypedRegion(end, document.getLength() - end, IDocument.DEFAULT_CONTENT_TYPE);
405   }
406
407   /*
408    * @see org.eclipse.jface.text.IDocumentPartitioner#computePartitioning(int, int)
409    */
410   public ITypedRegion[] computePartitioning(int offset, int length) {
411     List list = new ArrayList();
412
413     int end = offset + length;
414
415     int index = computeFlatNodeIndex(offset);
416     while (true) {
417       FlatNode prev = (index > 0) ? (FlatNode) nodes.get(index - 1) : null;
418
419       if (prev != null) {
420         if (prev.overlapsWith(offset, length)) {
421           list.add(new TypedRegion(prev.offset, prev.length, prev.type));
422         }
423
424         if (end <= prev.offset + prev.length) {
425           break;
426         }
427       }
428
429       FlatNode next = (index < nodes.size()) ? (FlatNode) nodes.get(index) : null;
430
431       if (next == null || offset < next.offset) {
432         int off0 = offset;
433         int off1 = offset + length;
434
435         if (prev != null && off0 < prev.offset + prev.length) {
436           off0 = prev.offset + prev.length;
437         }
438
439         if (next != null && next.offset < off1) {
440           off1 = next.offset;
441         }
442
443         if (off0 < off1) {
444           list.add(new TypedRegion(off0, off1 - off0, IDocument.DEFAULT_CONTENT_TYPE));
445         }
446       }
447
448       if (next == null) {
449         break;
450       }
451
452       ++index;
453     }
454
455     return (TypedRegion[]) list.toArray(new TypedRegion[list.size()]);
456   }
457
458   /**
459    * Computes the index in the list of flat nodes at which an flat node with the given offset would be inserted. The flat node is
460    * supposed to become the first in this list of all flat nodes with the same offset.
461    * 
462    * @param offset
463    *          the offset for which the index is computed
464    * @return the computed index
465    */
466   protected int computeFlatNodeIndex(int offset) {
467     if (nodes.size() == 0) {
468       return 0;
469     }
470
471     int left = 0, mid = 0;
472     int right = nodes.size() - 1;
473
474     FlatNode p = null;
475
476     while (left < right) {
477       mid = (left + right) / 2;
478
479       p = (FlatNode) nodes.get(mid);
480
481       if (offset < p.offset) {
482         right = (left == mid) ? left : mid - 1;
483       } else if (offset > p.offset) {
484         left = (right == mid) ? right : mid + 1;
485       } else if (offset == p.offset) {
486         left = right = mid;
487       }
488     }
489
490     int pos = left;
491     p = (FlatNode) nodes.get(pos);
492     if (offset > p.offset) {
493       // append to the end
494       pos++;
495     } else {
496       // entry will became the first of all entries with the same offset
497       do {
498         --pos;
499         if (pos < 0) {
500           break;
501         }
502         p = (FlatNode) nodes.get(pos);
503       } while (offset == p.offset);
504       ++pos;
505     }
506
507     return pos;
508   }
509
510   public boolean containsPosition(int offset, int length) {
511     int size = nodes.size();
512     if (size == 0) {
513       return false;
514     }
515
516     int index = computeFlatNodeIndex(offset);
517     if (index < size) {
518       FlatNode p = (FlatNode) nodes.get(index);
519
520       while (p.offset == offset) {
521         if (p.length == length) {
522           return true;
523         }
524
525         if (++index < size) {
526           p = (FlatNode) nodes.get(index);
527         } else {
528           break;
529         }
530       }
531     }
532
533     return false;
534   }
535
536   /**
537    * Helper method for tracking the minimal region containg all partition changes. If <code>offset</code> is smaller than the
538    * remembered offset, <code>offset</code> will from now on be remembered. If <code>offset + length</code> is greater than the
539    * remembered end offset, it will be remembered from now on.
540    * 
541    * @param offset
542    *          the offset
543    * @param length
544    *          the length
545    */
546   protected final void rememberRegion(int offset, int length) {
547     // remember start offset
548     if (regionStart == -1) {
549       regionStart = offset;
550     } else if (offset < regionStart) {
551       regionStart = offset;
552     }
553
554     // remember end offset
555     int endOffset = offset + length;
556
557     if (regionEnd == -1) {
558       regionEnd = endOffset;
559     } else if (endOffset > regionEnd) {
560       regionEnd = endOffset;
561     }
562   }
563
564   /**
565    * Creates the minimal region containing all partition changes using the remembered offsets.
566    * 
567    * @return the minimal region containing all the partition changes
568    */
569   protected final IRegion createRegion() {
570     if (regionStart == -1 || regionEnd == -1) {
571       return null;
572     }
573
574     return new Region(regionStart, regionEnd - regionStart);
575   }
576 }