a15d39c751fbb859f73aee26653303566066b860
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpdt / internal / ui / text / folding / DefaultJavaFoldingStructureProvider.java
1 /*******************************************************************************
2  * Copyright (c) 2000, 2003 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.folding;
12
13 import java.util.ArrayList;
14 import java.util.Collection;
15 import java.util.Collections;
16 import java.util.Comparator;
17 import java.util.HashMap;
18 import java.util.Iterator;
19 import java.util.LinkedHashMap;
20 import java.util.LinkedList;
21 import java.util.List;
22 import java.util.Map;
23
24 import net.sourceforge.phpdt.core.ElementChangedEvent;
25 import net.sourceforge.phpdt.core.ICompilationUnit;
26 import net.sourceforge.phpdt.core.IElementChangedListener;
27 import net.sourceforge.phpdt.core.IJavaElement;
28 import net.sourceforge.phpdt.core.IJavaElementDelta;
29 import net.sourceforge.phpdt.core.IMember;
30 import net.sourceforge.phpdt.core.IParent;
31 import net.sourceforge.phpdt.core.ISourceRange;
32 import net.sourceforge.phpdt.core.ISourceReference;
33 import net.sourceforge.phpdt.core.IType;
34 import net.sourceforge.phpdt.core.JavaCore;
35 import net.sourceforge.phpdt.core.JavaModelException;
36 import net.sourceforge.phpdt.core.ToolFactory;
37 import net.sourceforge.phpdt.core.compiler.IScanner;
38 import net.sourceforge.phpdt.core.compiler.ITerminalSymbols;
39 import net.sourceforge.phpdt.core.compiler.InvalidInputException;
40 import net.sourceforge.phpdt.internal.ui.text.DocumentCharacterIterator;
41 import net.sourceforge.phpdt.ui.IWorkingCopyManager;
42 import net.sourceforge.phpdt.ui.PreferenceConstants;
43 import net.sourceforge.phpdt.ui.text.folding.IJavaFoldingStructureProvider;
44 import net.sourceforge.phpeclipse.PHPeclipsePlugin;
45 import net.sourceforge.phpeclipse.phpeditor.PHPEditor;
46 import net.sourceforge.phpeclipse.phpeditor.PHPUnitEditor;
47
48 import org.eclipse.jface.preference.IPreferenceStore;
49 import org.eclipse.jface.text.Assert;
50 import org.eclipse.jface.text.BadLocationException;
51 import org.eclipse.jface.text.IDocument;
52 import org.eclipse.jface.text.IRegion;
53 import org.eclipse.jface.text.Position;
54 import org.eclipse.jface.text.Region;
55 import org.eclipse.jface.text.source.Annotation;
56 import org.eclipse.jface.text.source.IAnnotationModel;
57 import org.eclipse.jface.text.source.projection.IProjectionListener;
58 import org.eclipse.jface.text.source.projection.IProjectionPosition;
59 import org.eclipse.jface.text.source.projection.ProjectionAnnotation;
60 import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel;
61 import org.eclipse.jface.text.source.projection.ProjectionViewer;
62 import org.eclipse.ui.texteditor.IDocumentProvider;
63 import org.eclipse.ui.texteditor.ITextEditor;
64
65 /**
66  * Updates the projection model of a class file or compilation unit.
67  *
68  * @since 3.0
69  */
70 public class DefaultJavaFoldingStructureProvider implements IProjectionListener, IJavaFoldingStructureProvider {
71
72         private static class JavaProjectionAnnotation extends ProjectionAnnotation {
73
74                 private IJavaElement fJavaElement;
75
76                 private boolean fIsComment;
77
78                 public JavaProjectionAnnotation(IJavaElement element, boolean isCollapsed, boolean isComment) {
79                         super(isCollapsed);
80                         fJavaElement = element;
81                         fIsComment = isComment;
82                 }
83
84                 public IJavaElement getElement() {
85                         return fJavaElement;
86                 }
87
88                 public void setElement(IJavaElement element) {
89                         fJavaElement = element;
90                 }
91
92                 public boolean isComment() {
93                         return fIsComment;
94                 }
95
96                 public void setIsComment(boolean isComment) {
97                         fIsComment = isComment;
98                 }
99
100                 /*
101                  * @see java.lang.Object#toString()
102                  */
103                 public String toString() {
104                         return "JavaProjectionAnnotation:\n" + //$NON-NLS-1$
105                                         "\telement: \t" + fJavaElement.toString() + "\n" + //$NON-NLS-1$ //$NON-NLS-2$
106                                         "\tcollapsed: \t" + isCollapsed() + "\n" + //$NON-NLS-1$ //$NON-NLS-2$
107                                         "\tcomment: \t" + fIsComment + "\n"; //$NON-NLS-1$ //$NON-NLS-2$
108                 }
109         }
110
111         private static final class Tuple {
112                 JavaProjectionAnnotation annotation;
113
114                 Position position;
115
116                 Tuple(JavaProjectionAnnotation annotation, Position position) {
117                         this.annotation = annotation;
118                         this.position = position;
119                 }
120         }
121
122         private class ElementChangedListener implements IElementChangedListener {
123
124                 /*
125                  * @see org.eclipse.jdt.core.IElementChangedListener#elementChanged(org.eclipse.jdt.core.ElementChangedEvent)
126                  */
127                 public void elementChanged(ElementChangedEvent e) {
128                         IJavaElementDelta delta = findElement(fInput, e.getDelta());
129                         if (delta != null)
130                                 processDelta(delta);
131                 }
132
133                 private IJavaElementDelta findElement(IJavaElement target, IJavaElementDelta delta) {
134
135                         if (delta == null || target == null)
136                                 return null;
137
138                         IJavaElement element = delta.getElement();
139
140                         if (element.getElementType() > IJavaElement.CLASS_FILE)
141                                 return null;
142
143                         if (target.equals(element))
144                                 return delta;
145
146                         IJavaElementDelta[] children = delta.getAffectedChildren();
147
148                         for (int i = 0; i < children.length; i++) {
149                                 IJavaElementDelta d = findElement(target, children[i]);
150                                 if (d != null)
151                                         return d;
152                         }
153
154                         return null;
155                 }
156         }
157
158         /**
159          * Projection position that will return two foldable regions: one folding away
160          * the region from after the '/**' to the beginning of the content, the other
161          * from after the first content line until after the comment.
162          *
163          * @since 3.1
164          */
165         private static final class CommentPosition extends Position implements IProjectionPosition {
166                 CommentPosition(int offset, int length) {
167                         super(offset, length);
168                 }
169
170                 /*
171                  * @see org.eclipse.jface.text.source.projection.IProjectionPosition#computeFoldingRegions(org.eclipse.jface.text.IDocument)
172                  */
173                 public IRegion[] computeProjectionRegions(IDocument document) throws BadLocationException {
174                         DocumentCharacterIterator sequence = new DocumentCharacterIterator(document, offset, offset + length);
175                         int prefixEnd = 0;
176                         int contentStart = findFirstContent(sequence, prefixEnd);
177
178                         int firstLine = document.getLineOfOffset(offset + prefixEnd);
179                         int captionLine = document.getLineOfOffset(offset + contentStart);
180                         int lastLine = document.getLineOfOffset(offset + length);
181
182                         Assert.isTrue(firstLine <= captionLine, "first folded line is greater than the caption line"); //$NON-NLS-1$
183                         Assert.isTrue(captionLine <= lastLine, "caption line is greater than the last folded line"); //$NON-NLS-1$
184
185                         IRegion preRegion;
186                         if (firstLine < captionLine) {
187                                 // preRegion= new Region(offset + prefixEnd, contentStart - prefixEnd);
188                                 int preOffset = document.getLineOffset(firstLine);
189                                 IRegion preEndLineInfo = document.getLineInformation(captionLine);
190                                 int preEnd = preEndLineInfo.getOffset();
191                                 preRegion = new Region(preOffset, preEnd - preOffset);
192                         } else {
193                                 preRegion = null;
194                         }
195
196                         if (captionLine < lastLine) {
197                                 int postOffset = document.getLineOffset(captionLine + 1);
198                                 IRegion postRegion = new Region(postOffset, offset + length - postOffset);
199
200                                 if (preRegion == null)
201                                         return new IRegion[] { postRegion };
202
203                                 return new IRegion[] { preRegion, postRegion };
204                         }
205
206                         if (preRegion != null)
207                                 return new IRegion[] { preRegion };
208
209                         return null;
210                 }
211
212                 /**
213                  * Finds the offset of the first identifier part within <code>content</code>.
214                  * Returns 0 if none is found.
215                  *
216                  * @param content
217                  *          the content to search
218                  * @return the first index of a unicode identifier part, or zero if none can
219                  *         be found
220                  */
221                 private int findFirstContent(final CharSequence content, int prefixEnd) {
222                         int lenght = content.length();
223                         for (int i = prefixEnd; i < lenght; i++) {
224                                 if (Character.isUnicodeIdentifierPart(content.charAt(i)))
225                                         return i;
226                         }
227                         return 0;
228                 }
229
230                 // /**
231                 // * Finds the offset of the first identifier part within
232                 // <code>content</code>.
233                 // * Returns 0 if none is found.
234                 // *
235                 // * @param content the content to search
236                 // * @return the first index of a unicode identifier part, or zero if none
237                 // can
238                 // * be found
239                 // */
240                 // private int findPrefixEnd(final CharSequence content) {
241                 // // return the index after the leading '/*' or '/**'
242                 // int len= content.length();
243                 // int i= 0;
244                 // while (i < len && isWhiteSpace(content.charAt(i)))
245                 // i++;
246                 // if (len >= i + 2 && content.charAt(i) == '/' && content.charAt(i + 1) ==
247                 // '*')
248                 // if (len >= i + 3 && content.charAt(i + 2) == '*')
249                 // return i + 3;
250                 // else
251                 // return i + 2;
252                 // else
253                 // return i;
254                 // }
255                 //
256                 // private boolean isWhiteSpace(char c) {
257                 // return c == ' ' || c == '\t';
258                 // }
259
260                 /*
261                  * @see org.eclipse.jface.text.source.projection.IProjectionPosition#computeCaptionOffset(org.eclipse.jface.text.IDocument)
262                  */
263                 public int computeCaptionOffset(IDocument document) {
264                         // return 0;
265                         DocumentCharacterIterator sequence = new DocumentCharacterIterator(document, offset, offset + length);
266                         return findFirstContent(sequence, 0);
267                 }
268         }
269
270         /**
271          * Projection position that will return two foldable regions: one folding away
272          * the lines before the one containing the simple name of the java element,
273          * one folding away any lines after the caption.
274          *
275          * @since 3.1
276          */
277         private static final class JavaElementPosition extends Position implements IProjectionPosition {
278
279                 private IMember fMember;
280
281                 public JavaElementPosition(int offset, int length, IMember member) {
282                         super(offset, length);
283                         Assert.isNotNull(member);
284                         fMember = member;
285                 }
286
287                 public void setMember(IMember member) {
288                         Assert.isNotNull(member);
289                         fMember = member;
290                 }
291
292                 /*
293                  * @see org.eclipse.jface.text.source.projection.IProjectionPosition#computeFoldingRegions(org.eclipse.jface.text.IDocument)
294                  */
295                 public IRegion[] computeProjectionRegions(IDocument document) throws BadLocationException {
296                         int nameStart = offset;
297                         try {
298                                 /*
299                                  * The member's name range may not be correct. However, reconciling
300                                  * would trigger another element delta which would lead to reentrant
301                                  * situations. Therefore, we optimistically assume that the name range
302                                  * is correct, but double check the received lines below.
303                                  */
304                                 ISourceRange nameRange = fMember.getNameRange();
305                                 if (nameRange != null)
306                                         nameStart = nameRange.getOffset();
307
308                         } catch (JavaModelException e) {
309                                 // ignore and use default
310                         }
311
312                         int firstLine = document.getLineOfOffset(offset);
313                         int captionLine = document.getLineOfOffset(nameStart);
314                         int lastLine = document.getLineOfOffset(offset + length);
315
316                         /*
317                          * see comment above - adjust the caption line to be inside the entire
318                          * folded region, and rely on later element deltas to correct the name
319                          * range.
320                          */
321                         if (captionLine < firstLine)
322                                 captionLine = firstLine;
323                         if (captionLine > lastLine)
324                                 captionLine = lastLine;
325
326                         IRegion preRegion;
327                         if (firstLine < captionLine) {
328                                 int preOffset = document.getLineOffset(firstLine);
329                                 IRegion preEndLineInfo = document.getLineInformation(captionLine);
330                                 int preEnd = preEndLineInfo.getOffset();
331                                 preRegion = new Region(preOffset, preEnd - preOffset);
332                         } else {
333                                 preRegion = null;
334                         }
335
336                         if (captionLine < lastLine) {
337                                 int postOffset = document.getLineOffset(captionLine + 1);
338                                 IRegion postRegion = new Region(postOffset, offset + length - postOffset);
339
340                                 if (preRegion == null)
341                                         return new IRegion[] { postRegion };
342
343                                 return new IRegion[] { preRegion, postRegion };
344                         }
345
346                         if (preRegion != null)
347                                 return new IRegion[] { preRegion };
348
349                         return null;
350                 }
351
352                 /*
353                  * @see org.eclipse.jface.text.source.projection.IProjectionPosition#computeCaptionOffset(org.eclipse.jface.text.IDocument)
354                  */
355                 public int computeCaptionOffset(IDocument document) throws BadLocationException {
356                         int nameStart = offset;
357                         try {
358                                 // need a reconcile here?
359                                 ISourceRange nameRange = fMember.getNameRange();
360                                 if (nameRange != null)
361                                         nameStart = nameRange.getOffset();
362                         } catch (JavaModelException e) {
363                                 // ignore and use default
364                         }
365
366                         return nameStart - offset;
367                 }
368
369         }
370
371         private IDocument fCachedDocument;
372
373         private ProjectionAnnotationModel fCachedModel;
374
375         private ITextEditor fEditor;
376
377         private ProjectionViewer fViewer;
378
379         private IJavaElement fInput;
380
381         private IElementChangedListener fElementListener;
382
383         private boolean fAllowCollapsing = false;
384
385         private boolean fCollapseJavadoc = false;
386
387 //      private boolean fCollapseImportContainer = true;
388
389         private boolean fCollapseInnerTypes = true;
390
391         private boolean fCollapseMethods = false;
392
393         private boolean fCollapseHeaderComments = true;
394
395         /* caches for header comment extraction. */
396         private IType fFirstType;
397
398         private boolean fHasHeaderComment;
399
400         public DefaultJavaFoldingStructureProvider() {
401         }
402
403         public void install(ITextEditor editor, ProjectionViewer viewer) {
404                 if (editor instanceof PHPEditor) {
405                         fEditor = editor;
406                         fViewer = viewer;
407                         fViewer.addProjectionListener(this);
408                 }
409         }
410
411         public void uninstall() {
412                 if (isInstalled()) {
413                         projectionDisabled();
414                         fViewer.removeProjectionListener(this);
415                         fViewer = null;
416                         fEditor = null;
417                 }
418         }
419
420         protected boolean isInstalled() {
421                 return fEditor != null;
422         }
423
424         /*
425          * @see org.eclipse.jface.text.source.projection.IProjectionListener#projectionEnabled()
426          */
427         public void projectionEnabled() {
428                 // http://home.ott.oti.com/teams/wswb/anon/out/vms/index.html
429                 // projectionEnabled messages are not always paired with projectionDisabled
430                 // i.e. multiple enabled messages may be sent out.
431                 // we have to make sure that we disable first when getting an enable
432                 // message.
433                 projectionDisabled();
434
435                 if (fEditor instanceof PHPEditor) {
436                         initialize();
437                         fElementListener = new ElementChangedListener();
438                         JavaCore.addElementChangedListener(fElementListener);
439                 }
440         }
441
442         /*
443          * @see org.eclipse.jface.text.source.projection.IProjectionListener#projectionDisabled()
444          */
445         public void projectionDisabled() {
446                 fCachedDocument = null;
447                 if (fElementListener != null) {
448                         JavaCore.removeElementChangedListener(fElementListener);
449                         fElementListener = null;
450                 }
451         }
452
453         public void initialize() {
454
455                 if (!isInstalled())
456                         return;
457
458                 initializePreferences();
459
460                 try {
461
462                         IDocumentProvider provider = fEditor.getDocumentProvider();
463                         fCachedDocument = provider.getDocument(fEditor.getEditorInput());
464                         fAllowCollapsing = true;
465
466                         fFirstType = null;
467                         fHasHeaderComment = false;
468
469                         if (fEditor instanceof PHPUnitEditor) {
470                                 IWorkingCopyManager manager = PHPeclipsePlugin.getDefault().getWorkingCopyManager();
471                                 fInput = manager.getWorkingCopy(fEditor.getEditorInput());
472                         }
473                         // else if (fEditor instanceof ClassFileEditor) {
474                         // IClassFileEditorInput editorInput= (IClassFileEditorInput)
475                         // fEditor.getEditorInput();
476                         // fInput= editorInput.getClassFile();
477                         // }
478
479                         if (fInput != null) {
480                                 ProjectionAnnotationModel model = (ProjectionAnnotationModel) fEditor.getAdapter(ProjectionAnnotationModel.class);
481                                 if (model != null) {
482                                         fCachedModel = model;
483                                         if (fInput instanceof ICompilationUnit) {
484                                                 ICompilationUnit unit = (ICompilationUnit) fInput;
485                                                 synchronized (unit) {
486                                                         try {
487 //                                                              unit.reconcile(ICompilationUnit.NO_AST, false, null, null);
488                                                                 unit.reconcile();
489                                                         } catch (JavaModelException x) {
490                                                         }
491                                                 }
492                                         }
493
494                                         Map additions = computeAdditions((IParent) fInput);
495                                         /*
496                                          * Minimize the events being sent out - as this happens in the UI
497                                          * thread merge everything into one call.
498                                          */
499                                         List removals = new LinkedList();
500                                         Iterator existing = model.getAnnotationIterator();
501                                         while (existing.hasNext())
502                                                 removals.add(existing.next());
503                                         model.replaceAnnotations((Annotation[]) removals.toArray(new Annotation[removals.size()]), additions);
504                                 }
505                         }
506
507                 } finally {
508                         fCachedDocument = null;
509                         fCachedModel = null;
510                         fAllowCollapsing = false;
511
512                         fFirstType = null;
513                         fHasHeaderComment = false;
514                 }
515         }
516
517         private void initializePreferences() {
518                 IPreferenceStore store = PHPeclipsePlugin.getDefault().getPreferenceStore();
519                 fCollapseInnerTypes = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_INNERTYPES);
520 //              fCollapseImportContainer = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_IMPORTS);
521                 fCollapseJavadoc = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_JAVADOC);
522                 fCollapseMethods = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_METHODS);
523                 fCollapseHeaderComments = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_HEADERS);
524         }
525
526         private Map computeAdditions(IParent parent) {
527                 Map map = new LinkedHashMap(); // use a linked map to maintain ordering of
528                                                                                                                                                 // comments
529                 try {
530                         computeAdditions(parent.getChildren(), map);
531                 } catch (JavaModelException x) {
532                 }
533                 return map;
534         }
535
536         private void computeAdditions(IJavaElement[] elements, Map map) throws JavaModelException {
537                 for (int i = 0; i < elements.length; i++) {
538                         IJavaElement element = elements[i];
539
540                         computeAdditions(element, map);
541
542                         if (element instanceof IParent) {
543                                 IParent parent = (IParent) element;
544                                 computeAdditions(parent.getChildren(), map);
545                         }
546                 }
547         }
548
549         private void computeAdditions(IJavaElement element, Map map) {
550
551                 boolean createProjection = false;
552
553                 boolean collapse = false;
554                 switch (element.getElementType()) {
555
556 //              case IJavaElement.IMPORT_CONTAINER:
557 //                      collapse = fAllowCollapsing && fCollapseImportContainer;
558 //                      createProjection = true;
559 //                      break;
560                 case IJavaElement.TYPE:
561                         collapse = fAllowCollapsing && fCollapseInnerTypes && isInnerType((IType) element);
562                         createProjection = true;
563                         break;
564                 case IJavaElement.METHOD:
565                         collapse = fAllowCollapsing && fCollapseMethods;
566                         createProjection = true;
567                         break;
568                 }
569
570                 if (createProjection) {
571                         IRegion[] regions = computeProjectionRanges(element);
572                         if (regions != null) {
573                                 // comments
574                                 for (int i = 0; i < regions.length - 1; i++) {
575                                         Position position = createProjectionPosition(regions[i], null);
576                                         boolean commentCollapse;
577                                         if (position != null) {
578                                                 if (i == 0 && (regions.length > 2 || fHasHeaderComment) && element == fFirstType) {
579                                                         commentCollapse = fAllowCollapsing && fCollapseHeaderComments;
580                                                 } else {
581                                                         commentCollapse = fAllowCollapsing && fCollapseJavadoc;
582                                                 }
583                                                 map.put(new JavaProjectionAnnotation(element, commentCollapse, true), position);
584                                         }
585                                 }
586                                 // code
587                                 Position position = createProjectionPosition(regions[regions.length - 1], element);
588                                 if (position != null)
589                                         map.put(new JavaProjectionAnnotation(element, collapse, false), position);
590                         }
591                 }
592         }
593
594         private boolean isInnerType(IType type) {
595
596                 try {
597                         return type.isMember();
598                 } catch (JavaModelException x) {
599                         IJavaElement parent = type.getParent();
600                         if (parent != null) {
601                                 int parentType = parent.getElementType();
602                                 return (parentType != IJavaElement.COMPILATION_UNIT && parentType != IJavaElement.CLASS_FILE);
603                         }
604                 }
605
606                 return false;
607         }
608
609         /**
610          * Computes the projection ranges for a given <code>IJavaElement</code>.
611          * More than one range may be returned if the element has a leading comment
612          * which gets folded separately. If there are no foldable regions,
613          * <code>null</code> is returned.
614          *
615          * @param element
616          *          the java element that can be folded
617          * @return the regions to be folded, or <code>null</code> if there are none
618          */
619         private IRegion[] computeProjectionRanges(IJavaElement element) {
620
621                 try {
622                         if (element instanceof ISourceReference) {
623                                 ISourceReference reference = (ISourceReference) element;
624                                 ISourceRange range = reference.getSourceRange();
625
626                                 String contents = reference.getSource();
627                                 if (contents == null)
628                                         return null;
629
630                                 List regions = new ArrayList();
631                                 if (fFirstType == null && element instanceof IType) {
632                                         fFirstType = (IType) element;
633                                         IRegion headerComment = computeHeaderComment(fFirstType);
634                                         if (headerComment != null) {
635                                                 regions.add(headerComment);
636                                                 fHasHeaderComment = true;
637                                         }
638                                 }
639
640                                 IScanner scanner = ToolFactory.createScanner(true, false, false, false);
641                                 scanner.setSource(contents.toCharArray());
642                                 final int shift = range.getOffset();
643                                 int start = shift;
644                                 while (true) {
645
646                                         int token = scanner.getNextToken();
647                                         start = shift + scanner.getCurrentTokenStartPosition();
648
649                                         switch (token) {
650                                         case ITerminalSymbols.TokenNameCOMMENT_PHPDOC:
651                                         case ITerminalSymbols.TokenNameCOMMENT_BLOCK: {
652                                                 int end = shift + scanner.getCurrentTokenEndPosition() + 1;
653                                                 regions.add(new Region(start, end - start));
654                                         }
655                                         case ITerminalSymbols.TokenNameCOMMENT_LINE:
656                                                 continue;
657                                         }
658
659                                         break;
660                                 }
661
662                                 regions.add(new Region(start, shift + range.getLength() - start));
663
664                                 if (regions.size() > 0) {
665                                         IRegion[] result = new IRegion[regions.size()];
666                                         regions.toArray(result);
667                                         return result;
668                                 }
669                         }
670                 } catch (JavaModelException e) {
671                 } catch (InvalidInputException e) {
672                 }
673
674                 return null;
675         }
676
677         private IRegion computeHeaderComment(IType type) throws JavaModelException {
678                 if (fCachedDocument == null)
679                         return null;
680
681                 // search at most up to the first type
682                 ISourceRange range = type.getSourceRange();
683                 if (range == null)
684                         return null;
685                 int start = 0;
686                 int end = range.getOffset();
687
688                 if (fInput instanceof ISourceReference) {
689                         String content;
690                         try {
691                                 content = fCachedDocument.get(start, end - start);
692                         } catch (BadLocationException e) {
693                                 return null; // ignore header comment in that case
694                         }
695
696                         /*
697                          * code adapted from CommentFormattingStrategy: scan the header content up
698                          * to the first type. Once a comment is found, accumulate any additional
699                          * comments up to the stop condition. The stop condition is reaching a
700                          * package declaration, import container, or the end of the input.
701                          */
702                         IScanner scanner = ToolFactory.createScanner(true, false, false, false);
703                         scanner.setSource(content.toCharArray());
704
705                         int headerStart = -1;
706                         int headerEnd = -1;
707                         try {
708                                 boolean foundComment = false;
709                                 int terminal = scanner.getNextToken();
710                                 while (terminal != ITerminalSymbols.TokenNameEOF
711                                                 && !(terminal == ITerminalSymbols.TokenNameclass || terminal == ITerminalSymbols.TokenNameinterface || foundComment)) {
712
713                                         if (terminal == ITerminalSymbols.TokenNameCOMMENT_PHPDOC || terminal == ITerminalSymbols.TokenNameCOMMENT_BLOCK
714                                                         || terminal == ITerminalSymbols.TokenNameCOMMENT_LINE) {
715                                                 if (!foundComment)
716                                                         headerStart = scanner.getCurrentTokenStartPosition();
717                                                 headerEnd = scanner.getCurrentTokenEndPosition();
718                                                 foundComment = true;
719                                         }
720                                         terminal = scanner.getNextToken();
721                                 }
722
723                         } catch (InvalidInputException ex) {
724                                 return null;
725                         }
726
727                         if (headerEnd != -1) {
728                                 return new Region(headerStart, headerEnd - headerStart);
729                         }
730                 }
731                 return null;
732         }
733
734         private Position createProjectionPosition(IRegion region, IJavaElement element) {
735
736                 if (fCachedDocument == null)
737                         return null;
738
739                 try {
740
741                         int start = fCachedDocument.getLineOfOffset(region.getOffset());
742                         int end = fCachedDocument.getLineOfOffset(region.getOffset() + region.getLength());
743                         if (start != end) {
744                                 int offset = fCachedDocument.getLineOffset(start);
745                                 int endOffset;
746                                 if (fCachedDocument.getNumberOfLines() > end + 1)
747                                         endOffset = fCachedDocument.getLineOffset(end + 1);
748                                 else if (end > start)
749                                         endOffset = fCachedDocument.getLineOffset(end) + fCachedDocument.getLineLength(end);
750                                 else
751                                         return null;
752                                 if (element instanceof IMember)
753                                         return new JavaElementPosition(offset, endOffset - offset, (IMember) element);
754                                 else
755                                         return new CommentPosition(offset, endOffset - offset);
756                         }
757
758                 } catch (BadLocationException x) {
759                 }
760
761                 return null;
762         }
763
764         protected void processDelta(IJavaElementDelta delta) {
765
766                 if (!isInstalled())
767                         return;
768
769                 if ((delta.getFlags() & (IJavaElementDelta.F_CONTENT | IJavaElementDelta.F_CHILDREN)) == 0)
770                         return;
771
772                 ProjectionAnnotationModel model = (ProjectionAnnotationModel) fEditor.getAdapter(ProjectionAnnotationModel.class);
773                 if (model == null)
774                         return;
775
776                 try {
777
778                         IDocumentProvider provider = fEditor.getDocumentProvider();
779                         fCachedDocument = provider.getDocument(fEditor.getEditorInput());
780                         fCachedModel = model;
781                         fAllowCollapsing = false;
782
783                         fFirstType = null;
784                         fHasHeaderComment = false;
785
786                         Map additions = new HashMap();
787                         List deletions = new ArrayList();
788                         List updates = new ArrayList();
789
790                         Map updated = computeAdditions((IParent) fInput);
791                         Map previous = createAnnotationMap(model);
792
793                         Iterator e = updated.keySet().iterator();
794                         while (e.hasNext()) {
795                                 JavaProjectionAnnotation newAnnotation = (JavaProjectionAnnotation) e.next();
796                                 IJavaElement element = newAnnotation.getElement();
797                                 Position newPosition = (Position) updated.get(newAnnotation);
798
799                                 List annotations = (List) previous.get(element);
800                                 if (annotations == null) {
801
802                                         additions.put(newAnnotation, newPosition);
803
804                                 } else {
805                                         Iterator x = annotations.iterator();
806                                         boolean matched = false;
807                                         while (x.hasNext()) {
808                                                 Tuple tuple = (Tuple) x.next();
809                                                 JavaProjectionAnnotation existingAnnotation = tuple.annotation;
810                                                 Position existingPosition = tuple.position;
811                                                 if (newAnnotation.isComment() == existingAnnotation.isComment()) {
812                                                         if (existingPosition != null && (!newPosition.equals(existingPosition))) {
813                                                                 existingPosition.setOffset(newPosition.getOffset());
814                                                                 existingPosition.setLength(newPosition.getLength());
815                                                                 updates.add(existingAnnotation);
816                                                         }
817                                                         matched = true;
818                                                         x.remove();
819                                                         break;
820                                                 }
821                                         }
822                                         if (!matched)
823                                                 additions.put(newAnnotation, newPosition);
824
825                                         if (annotations.isEmpty())
826                                                 previous.remove(element);
827                                 }
828                         }
829
830                         e = previous.values().iterator();
831                         while (e.hasNext()) {
832                                 List list = (List) e.next();
833                                 int size = list.size();
834                                 for (int i = 0; i < size; i++)
835                                         deletions.add(((Tuple) list.get(i)).annotation);
836                         }
837
838                         match(deletions, additions, updates);
839
840                         Annotation[] removals = new Annotation[deletions.size()];
841                         deletions.toArray(removals);
842                         Annotation[] changes = new Annotation[updates.size()];
843                         updates.toArray(changes);
844                         model.modifyAnnotations(removals, additions, changes);
845
846                 } finally {
847                         fCachedDocument = null;
848                         fAllowCollapsing = true;
849                         fCachedModel = null;
850
851                         fFirstType = null;
852                         fHasHeaderComment = false;
853                 }
854         }
855
856         /**
857          * Matches deleted annotations to changed or added ones. A deleted
858          * annotation/position tuple that has a matching addition / change is updated
859          * and marked as changed. The matching tuple is not added (for additions) or
860          * marked as deletion instead (for changes). The result is that more
861          * annotations are changed and fewer get deleted/re-added.
862          */
863         private void match(List deletions, Map additions, List changes) {
864                 if (deletions.isEmpty() || (additions.isEmpty() && changes.isEmpty()))
865                         return;
866
867                 List newDeletions = new ArrayList();
868                 List newChanges = new ArrayList();
869
870                 Iterator deletionIterator = deletions.iterator();
871                 while (deletionIterator.hasNext()) {
872                         JavaProjectionAnnotation deleted = (JavaProjectionAnnotation) deletionIterator.next();
873                         Position deletedPosition = fCachedModel.getPosition(deleted);
874                         if (deletedPosition == null)
875                                 continue;
876
877                         Tuple deletedTuple = new Tuple(deleted, deletedPosition);
878
879                         Tuple match = findMatch(deletedTuple, changes, null);
880                         boolean addToDeletions = true;
881                         if (match == null) {
882                                 match = findMatch(deletedTuple, additions.keySet(), additions);
883                                 addToDeletions = false;
884                         }
885
886                         if (match != null) {
887                                 IJavaElement element = match.annotation.getElement();
888                                 deleted.setElement(element);
889                                 deletedPosition.setLength(match.position.getLength());
890                                 if (deletedPosition instanceof JavaElementPosition && element instanceof IMember) {
891                                         JavaElementPosition jep = (JavaElementPosition) deletedPosition;
892                                         jep.setMember((IMember) element);
893                                 }
894
895                                 deletionIterator.remove();
896                                 newChanges.add(deleted);
897
898                                 if (addToDeletions)
899                                         newDeletions.add(match.annotation);
900                         }
901                 }
902
903                 deletions.addAll(newDeletions);
904                 changes.addAll(newChanges);
905         }
906
907         /**
908          * Finds a match for <code>tuple</code> in a collection of annotations. The
909          * positions for the <code>JavaProjectionAnnotation</code> instances in
910          * <code>annotations</code> can be found in the passed
911          * <code>positionMap</code> or <code>fCachedModel</code> if
912          * <code>positionMap</code> is <code>null</code>.
913          * <p>
914          * A tuple is said to match another if their annotations have the same comment
915          * flag and their position offsets are equal.
916          * </p>
917          * <p>
918          * If a match is found, the annotation gets removed from
919          * <code>annotations</code>.
920          * </p>
921          *
922          * @param tuple
923          *          the tuple for which we want to find a match
924          * @param annotations
925          *          collection of <code>JavaProjectionAnnotation</code>
926          * @param positionMap
927          *          a <code>Map&lt;Annotation, Position&gt;</code> or
928          *          <code>null</code>
929          * @return a matching tuple or <code>null</code> for no match
930          */
931         private Tuple findMatch(Tuple tuple, Collection annotations, Map positionMap) {
932                 Iterator it = annotations.iterator();
933                 while (it.hasNext()) {
934                         JavaProjectionAnnotation annotation = (JavaProjectionAnnotation) it.next();
935                         if (tuple.annotation.isComment() == annotation.isComment()) {
936                                 Position position = positionMap == null ? fCachedModel.getPosition(annotation) : (Position) positionMap.get(annotation);
937                                 if (position == null)
938                                         continue;
939
940                                 if (tuple.position.getOffset() == position.getOffset()) {
941                                         it.remove();
942                                         return new Tuple(annotation, position);
943                                 }
944                         }
945                 }
946
947                 return null;
948         }
949
950         private Map createAnnotationMap(IAnnotationModel model) {
951                 Map map = new HashMap();
952                 Iterator e = model.getAnnotationIterator();
953                 while (e.hasNext()) {
954                         Object annotation = e.next();
955                         if (annotation instanceof JavaProjectionAnnotation) {
956                                 JavaProjectionAnnotation java = (JavaProjectionAnnotation) annotation;
957                                 Position position = model.getPosition(java);
958                                 Assert.isNotNull(position);
959                                 List list = (List) map.get(java.getElement());
960                                 if (list == null) {
961                                         list = new ArrayList(2);
962                                         map.put(java.getElement(), list);
963                                 }
964                                 list.add(new Tuple(java, position));
965                         }
966                 }
967
968                 Comparator comparator = new Comparator() {
969                         public int compare(Object o1, Object o2) {
970                                 return ((Tuple) o1).position.getOffset() - ((Tuple) o2).position.getOffset();
971                         }
972                 };
973                 for (Iterator it = map.values().iterator(); it.hasNext();) {
974                         List list = (List) it.next();
975                         Collections.sort(list, comparator);
976                 }
977                 return map;
978         }
979 }