misc
[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.1 2004-09-02 18:26:29 jsurfer 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 = true;
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
107     IToken token = scanner.nextToken();
108     while (!token.isEOF()) {
109       String contentType = getTokenContentType(token);
110
111       if (isSupportedContentType(contentType)) {
112         addInnerRegion(createNode(contentType, scanner.getTokenOffset(), scanner.getTokenLength()));
113       }
114
115       token = scanner.nextToken();
116     }
117   }
118
119   /*
120    * @see org.eclipse.jface.text.IDocumentPartitioner#documentAboutToBeChanged(DocumentEvent)
121    */
122   public void documentAboutToBeChanged(DocumentEvent event) {
123     regionStart = regionEnd = -1;
124   }
125
126   /*
127    * @see org.eclipse.jface.text.IDocumentPartitioner#documentChanged(DocumentEvent)
128    */
129   public boolean documentChanged(DocumentEvent event) {
130     return (documentChanged2(event) != null);
131   }
132
133   /*
134    * @see org.eclipse.jface.text.IDocumentPartitionerExtension#documentChanged2(DocumentEvent)
135    */
136   public IRegion documentChanged2(DocumentEvent event) {
137     int first = fixupPartitions(event);
138
139     FlatNode[] category = (FlatNode[]) nodes.toArray(new FlatNode[nodes.size()]);
140
141     // repartition changed region
142
143     String contentType = IDocument.DEFAULT_CONTENT_TYPE;
144
145     int offset;
146
147     if (first == 0) {
148       offset = 0; // Bug #697414: first offset
149     } else {
150       offset = event.getOffset();
151
152       FlatNode partition = category[first - 1];
153       if (partition.includes(offset)) {
154         offset = partition.offset;
155         contentType = partition.type;
156         --first;
157       } else if (offset == partition.offset + partition.length) {
158         offset = partition.offset;
159         contentType = partition.type;
160         --first;
161       } else {
162         offset = partition.offset + partition.length;
163       }
164     }
165
166     // should not be changed since last conversion
167     // category = (FlatNode[]) nodes.toArray(new FlatNode[nodes.size()]);
168     if (DEBUG) {
169       Assert.isTrue(offset >= 0, Integer.toString(offset));
170     }
171     scanner.setPartialRange(document, offset, document.getLength(), contentType, offset);
172
173     int lastScannedPosition = offset;
174     IToken token = scanner.nextToken();
175     while (!token.isEOF()) {
176       contentType = getTokenContentType(token);
177
178       if (!isSupportedContentType(contentType)) {
179         token = scanner.nextToken();
180         continue;
181       }
182
183       offset = scanner.getTokenOffset();
184       if (DEBUG) {
185         Assert.isTrue(offset >= 0, scanner.toString());
186       }
187       int length = scanner.getTokenLength();
188
189       lastScannedPosition = offset + length;
190
191       // remove all affected positions
192       while (first < category.length) {
193         FlatNode p = category[first];
194         if (p.offset + p.length < lastScannedPosition
195             || (p.overlapsWith(offset, length) && (!containsPosition(offset, length) || !contentType.equals(p.type)))) {
196           removeInnerRegion(p);
197           rememberRegion(p.offset, p.length);
198           ++first;
199         } else {
200           break;
201         }
202       }
203
204       // if position already exists we are done
205       if (containsPosition(offset, length)) {
206         if (lastScannedPosition > event.getOffset()) {
207           // TODO: optional repartition till end of doc
208           return createRegion();
209         }
210
211         ++first;
212       } else {
213         // insert the new type position
214         addInnerRegion(createNode(contentType, offset, length));
215         rememberRegion(offset, length);
216       }
217       //            try {
218       token = scanner.nextToken();
219       //            } catch (ArrayIndexOutOfBoundsException e) {
220       //              System.out.println(this.getClass().toString());
221       //              throw e;
222       //            }
223     }
224
225     // remove all positions behind lastScannedPosition
226     // since there aren't any further types
227
228     // Do not need to recalculate (lost remove events)!
229     // first = computeIndexInInnerDocuments(lastScannedPosition);
230     while (first < category.length) {
231       FlatNode p = category[first++];
232       removeInnerRegion(p);
233       rememberRegion(p.offset, p.length);
234     }
235
236     return createRegion();
237   }
238
239   protected int fixupPartitions(DocumentEvent event) {
240     int offset = event.getOffset();
241     int length = event.getLength();
242     int end = offset + length;
243
244     // fixup flat nodes laying on change boundaries
245
246     int first = computeFlatNodeIndex(offset);
247     if (first > 0) {
248       FlatNode p = (FlatNode) nodes.get(first - 1);
249
250       int right = p.offset + p.length;
251       if (offset < right) {
252         // change overlaps with partition
253         if (end < right) {
254           // cahnge completely inside partition
255           String text = event.getText();
256           p.length -= length;
257           if (text != null) {
258             p.length += text.length();
259           }
260         } else {
261           // cut partition at right
262           int cut = p.offset + p.length - offset;
263           p.length -= cut;
264         }
265       }
266     }
267
268     int last = computeFlatNodeIndex(end);
269     if (first < last) {
270       FlatNode p = (FlatNode) nodes.get(last - 1);
271
272       int right = p.offset + p.length;
273       if (end < right) {
274         // cut partition at left
275         int cut = end - p.offset;
276         p.length -= cut;
277         p.offset = offset;
278
279         String text = event.getText();
280         if (text != null) {
281           p.offset += text.length();
282         }
283
284         --last;
285       }
286     }
287
288     // fixup flat nodes laying afrer change
289
290     String text = event.getText();
291     if (text != null) {
292       length -= text.length();
293     }
294
295     for (int i = last, size = nodes.size(); i < size; i++) {
296       ((FlatNode) nodes.get(i)).offset -= length;
297     }
298
299     // delete flat nodes laying completely inside change boundaries
300
301     if (first < last) {
302       do {
303         deleteInnerRegion((FlatNode) nodes.get(--last));
304       } while (first < last);
305
306       rememberRegion(offset, 0);
307     }
308
309     return first;
310   }
311
312   /**
313    * Returns whether the given type is one of the legal content types.
314    * 
315    * @param contentType
316    *          the content type to check
317    * @return <code>true</code> if the content type is a legal content type
318    */
319   protected boolean isSupportedContentType(String contentType) {
320     /* TODO: implementation */
321     //          if (contentType != null) {
322     //                  for (int i= 0; i < fLegalContentTypes.length; i++) {
323     //                          if (fLegalContentTypes[i].equals(contentType)) {
324     //                                  return true;
325     //                          }
326     //                  }
327     //          }
328     //          return false;
329     return (contentType != null);
330   }
331
332   /**
333    * 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
334    * that it is the encoded content type.
335    * 
336    * @param token
337    *          the token whose content type is to be determined
338    * @return the token's content type
339    */
340   protected String getTokenContentType(IToken token) {
341     Object data = token.getData();
342     if (data instanceof String) {
343       return (String) data;
344     }
345
346     return null;
347   }
348
349   /*
350    * @see org.eclipse.jface.text.IDocumentPartitioner#getLegalContentTypes()
351    */
352   public String[] getLegalContentTypes() {
353     // TODO: implementation
354     return null;
355   }
356
357   /*
358    * @see org.eclipse.jface.text.IDocumentPartitioner#getContentType(int)
359    */
360   public String getContentType(int offset) {
361     return getPartition(offset).getType();
362   }
363
364   /*
365    * @see org.eclipse.jface.text.IDocumentPartitioner#getPartition(int)
366    */
367   public ITypedRegion getPartition(int offset) {
368     if (nodes.size() == 0) {
369       return new TypedRegion(0, document.getLength(), IDocument.DEFAULT_CONTENT_TYPE);
370     }
371
372     int index = computeFlatNodeIndex(offset);
373     if (index < nodes.size()) {
374       FlatNode next = (FlatNode) nodes.get(index);
375
376       if (offset == next.offset) {
377         return new TypedRegion(next.offset, next.length, next.type);
378       }
379
380       if (index == 0) {
381         return new TypedRegion(0, next.offset, IDocument.DEFAULT_CONTENT_TYPE);
382       }
383
384       FlatNode prev = (FlatNode) nodes.get(index - 1);
385
386       if (prev.includes(offset)) {
387         return new TypedRegion(prev.offset, prev.length, prev.type);
388       }
389
390       int end = prev.offset + prev.length;
391       return new TypedRegion(end, next.offset - end, IDocument.DEFAULT_CONTENT_TYPE);
392     }
393
394     FlatNode prev = (FlatNode) nodes.get(nodes.size() - 1);
395
396     if (prev.includes(offset)) {
397       return new TypedRegion(prev.offset, prev.length, prev.type);
398     }
399
400     int end = prev.offset + prev.length;
401
402     return new TypedRegion(end, document.getLength() - end, IDocument.DEFAULT_CONTENT_TYPE);
403   }
404
405   /*
406    * @see org.eclipse.jface.text.IDocumentPartitioner#computePartitioning(int, int)
407    */
408   public ITypedRegion[] computePartitioning(int offset, int length) {
409     List list = new ArrayList();
410
411     int end = offset + length;
412
413     int index = computeFlatNodeIndex(offset);
414     while (true) {
415       FlatNode prev = (index > 0) ? (FlatNode) nodes.get(index - 1) : null;
416
417       if (prev != null) {
418         if (prev.overlapsWith(offset, length)) {
419           list.add(new TypedRegion(prev.offset, prev.length, prev.type));
420         }
421
422         if (end <= prev.offset + prev.length) {
423           break;
424         }
425       }
426
427       FlatNode next = (index < nodes.size()) ? (FlatNode) nodes.get(index) : null;
428
429       if (next == null || offset < next.offset) {
430         int off0 = offset;
431         int off1 = offset + length;
432
433         if (prev != null && off0 < prev.offset + prev.length) {
434           off0 = prev.offset + prev.length;
435         }
436
437         if (next != null && next.offset < off1) {
438           off1 = next.offset;
439         }
440
441         if (off0 < off1) {
442           list.add(new TypedRegion(off0, off1 - off0, IDocument.DEFAULT_CONTENT_TYPE));
443         }
444       }
445
446       if (next == null) {
447         break;
448       }
449
450       ++index;
451     }
452
453     return (TypedRegion[]) list.toArray(new TypedRegion[list.size()]);
454   }
455
456   /**
457    * 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
458    * supposed to become the first in this list of all flat nodes with the same offset.
459    * 
460    * @param offset
461    *          the offset for which the index is computed
462    * @return the computed index
463    */
464   protected int computeFlatNodeIndex(int offset) {
465     if (nodes.size() == 0) {
466       return 0;
467     }
468
469     int left = 0, mid = 0;
470     int right = nodes.size() - 1;
471
472     FlatNode p = null;
473
474     while (left < right) {
475       mid = (left + right) / 2;
476
477       p = (FlatNode) nodes.get(mid);
478
479       if (offset < p.offset) {
480         right = (left == mid) ? left : mid - 1;
481       } else if (offset > p.offset) {
482         left = (right == mid) ? right : mid + 1;
483       } else if (offset == p.offset) {
484         left = right = mid;
485       }
486     }
487
488     int pos = left;
489     p = (FlatNode) nodes.get(pos);
490     if (offset > p.offset) {
491       // append to the end
492       pos++;
493     } else {
494       // entry will became the first of all entries with the same offset
495       do {
496         --pos;
497         if (pos < 0) {
498           break;
499         }
500         p = (FlatNode) nodes.get(pos);
501       } while (offset == p.offset);
502       ++pos;
503     }
504
505     return pos;
506   }
507
508   public boolean containsPosition(int offset, int length) {
509     int size = nodes.size();
510     if (size == 0) {
511       return false;
512     }
513
514     int index = computeFlatNodeIndex(offset);
515     if (index < size) {
516       FlatNode p = (FlatNode) nodes.get(index);
517
518       while (p.offset == offset) {
519         if (p.length == length) {
520           return true;
521         }
522
523         if (++index < size) {
524           p = (FlatNode) nodes.get(index);
525         } else {
526           break;
527         }
528       }
529     }
530
531     return false;
532   }
533
534   /**
535    * Helper method for tracking the minimal region containg all partition changes. If <code>offset</code> is smaller than the
536    * remembered offset, <code>offset</code> will from now on be remembered. If <code>offset + length</code> is greater than the
537    * remembered end offset, it will be remembered from now on.
538    * 
539    * @param offset
540    *          the offset
541    * @param length
542    *          the length
543    */
544   protected final void rememberRegion(int offset, int length) {
545     // remember start offset
546     if (regionStart == -1) {
547       regionStart = offset;
548     } else if (offset < regionStart) {
549       regionStart = offset;
550     }
551
552     // remember end offset
553     int endOffset = offset + length;
554
555     if (regionEnd == -1) {
556       regionEnd = endOffset;
557     } else if (endOffset > regionEnd) {
558       regionEnd = endOffset;
559     }
560   }
561
562   /**
563    * Creates the minimal region containing all partition changes using the remembered offsets.
564    * 
565    * @return the minimal region containing all the partition changes
566    */
567   protected final IRegion createRegion() {
568     if (regionStart == -1 || regionEnd == -1) {
569       return null;
570     }
571
572     return new Region(regionStart, regionEnd - regionStart);
573   }
574 }