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