Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: 3f1641c36a75788ab73b618db4f983b719517f4c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
/*******************************************************************************
 * Copyright (c) 2000, 2004 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jface.text.projection;


import java.util.ArrayList;
import java.util.List;

import org.eclipse.jface.text.AbstractDocument;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DefaultLineTracker;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.ILineTracker;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextStore;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;


/**
 * A <code>ProjectionDocument</code> represents a projection of its master
 * document. The contents of a projection document is a sequence of fragments of
 * the master document, i.e. the projection document can be thought as being
 * constructed from the master document by not copying the whole master document
 * but omitting several ranges of the master document.
 * <p>
 * The projection document indirectly utilizes its master document as
 * <code>ITextStore</code> by means of a <code>ProjectionTextStore</code>.
 * <p>
 * The content of a projection document can be changed in two ways. Either by a
 * text replace applied to the master document or the projection document. Or by
 * changing the projection between the master document and the projection
 * document. For the latter the two methods <code>addMasterDocumentRange</code>
 * and <code>removeMasterDocumentRange</code> are provided. For any
 * manipulation, the projection document sends out a
 * {@link org.eclipse.jface.text.projection.ProjectionDocumentEvent} describing
 * the change.
 * <p>
 * Clients are not supposed to directly instantiate this class. In order to
 * obtain a projection document, a
 * {@link org.eclipse.jface.text.projection.ProjectionDocumentManager}should be
 * used. This class is not intended to be subclassed outside of its origin
 * package.
 * 
 * @since 3.0
 */
public class ProjectionDocument extends AbstractDocument {
	
	
	/**
	 * Prefix of the name of the position category used to keep track of the master
	 * document's fragments that correspond to the segments of the projection
	 * document.
	 */
	private final static String FRAGMENTS_CATEGORY_PREFIX= "__fragmentsCategory"; //$NON-NLS-1$

	/**
	 * Name of the position category used to keep track of the project
	 * document's segments that correspond to the fragments of the master
	 * document.
	 */
	private final static String SEGMENTS_CATEGORY= "__segmentsCategory"; //$NON-NLS-1$

	
	/** The master document */
	private IDocument fMasterDocument;
	/** The master document as document extension */
	private IDocumentExtension fMasterDocumentExtension;
	/** The fragments' position category */
	private String fFragmentsCategory;
	/** The segment's position category */
	private String fSegmentsCategory;
	/** The document event issued by the master document */
	private DocumentEvent fMasterEvent;
	/** The document event to be issued by the projection document */
	private ProjectionDocumentEvent fSlaveEvent;
	/** The original document event generated by a direct manipulation of this projection document */
	private DocumentEvent fOriginalEvent;
	/** Indicates whether the projection document initiated a master document update or not */
	private boolean fIsUpdating= false;	
	/** Indicated whether the projection document is in auto expand mode nor not */
	private boolean fIsAutoExpanding= false;
	/** The position updater for the segments */
	private SegmentUpdater fSegmentUpdater;
	/** The position updater for the fragments */
	private FragmentUpdater fFragmentsUpdater;
	/** The projection mapping */
	private ProjectionMapping fMapping;
	
	/**
	 * Creates a projection document for the given master document.
	 *
	 * @param masterDocument the master document
	 */
	public ProjectionDocument(IDocument masterDocument) {
		super();
		
		fMasterDocument= masterDocument;
		if (fMasterDocument instanceof IDocumentExtension) 
			fMasterDocumentExtension= (IDocumentExtension) fMasterDocument;
		
		fSegmentsCategory= SEGMENTS_CATEGORY;
		fFragmentsCategory= FRAGMENTS_CATEGORY_PREFIX + hashCode();
		fMasterDocument.addPositionCategory(fFragmentsCategory);
		fFragmentsUpdater= new FragmentUpdater(fFragmentsCategory);
		fMasterDocument.addPositionUpdater(fFragmentsUpdater);		
		
		fMapping= new ProjectionMapping(masterDocument, fFragmentsCategory, this, fSegmentsCategory);
		
		ITextStore s= new ProjectionTextStore(masterDocument, fMapping);
		ILineTracker tracker= new DefaultLineTracker();
		
		setTextStore(s);
		setLineTracker(tracker);
		
		completeInitialization();
		
		initializeProjection();
		tracker.set(s.get(0, s.getLength()));
	}
	
	/**
	 * Disposes this projection document.
	 */
	public void dispose() {
		fMasterDocument.removePositionUpdater(fFragmentsUpdater);
		try {
			fMasterDocument.removePositionCategory(fFragmentsCategory);
		} catch (BadPositionCategoryException x) {
			// allow multiple dispose calls
		}
	}
		
	private void internalError() {
		throw new IllegalStateException();
	}
	
	/**
	 * Returns the fragments of the master documents.
	 * 
	 * @return the fragment of the master document
	 */
	protected final Position[] getFragments() {
		try {
			return fMasterDocument.getPositions(fFragmentsCategory);
		} catch (BadPositionCategoryException e) {
			internalError();
		}
		// unreachable
		return null;
	}
	
	/**
	 * Returns the segments of this projection document.
	 * 
	 * @return the segments of this projection document
	 */
	protected final Position[] getSegments() {
		try {
			return getPositions(fSegmentsCategory);
		} catch (BadPositionCategoryException e) {
			internalError();
		}
		// unreachable
		return null;
	}
	
	/**
	 * Returns the projection mapping used by this document.
	 * 
	 * @return the projection mapping used by this document
	 */
	public ProjectionMapping getProjectionMapping(){
		return fMapping;
	}
	
	/**
	 * Returns the master document of this projection document.
	 * 
	 * @return the master document of this projection document
	 */
	public IDocument getMasterDocument() {
		return fMasterDocument;
	}

	/**
	 * Initializes the projection document from the master document based on
	 * the master's fragments.
	 */
	private void initializeProjection() {
		
		try {
			
			addPositionCategory(fSegmentsCategory);
			fSegmentUpdater= new SegmentUpdater(fSegmentsCategory);
			addPositionUpdater(fSegmentUpdater);
			
			int offset= 0;
			Position[] fragments= getFragments();
			for (int i= 0; i < fragments.length; i++) {
				Fragment fragment= (Fragment) fragments[i];
				Segment segment= new Segment(offset, fragment.getLength());
				segment.fragment= fragment;
				addPosition(fSegmentsCategory, segment);
				offset += fragment.length;
			}
			
		} catch (BadPositionCategoryException x) {
			internalError();
		} catch (BadLocationException x) {
			internalError();
		}
	}
	
	/**
	 * Creates a segment for the given fragment at the given position inside the list of segments.
	 * 
	 * @param fragment the corresponding fragment
	 * @param index the index in the list of segments
	 * @return the created segment
	 * @throws BadLocationException in case the fragment is invalid
	 * @throws BadPositionCategoryException in case the segment category is invalid
	 */
	private Segment createSegmentFor(Fragment fragment, int index) throws BadLocationException, BadPositionCategoryException {
		
		int offset= 0;
		if (index > 0) {
			Position[] segments= getSegments();
			Segment segment= (Segment) segments[index - 1];
			offset= segment.getOffset() + segment.getLength();
		}
		
		Segment segment= new Segment(offset, 0);
		segment.fragment= fragment;
		fragment.segment= segment;
		addPosition(fSegmentsCategory, segment);
		return segment;
	}
	
	/**
	 * Adds the given range of the master document to this projection document.
	 * 
	 * @param offsetInMaster offset of the master document range
	 * @param lengthInMaster length of the master document range
	 * @param masterDocumentEvent the master document event that causes this
	 *            projection change or <code>null</code> if none
	 * @throws BadLocationException if the given range is invalid in the master
	 *             document
	 */
	private void internalAddMasterDocumentRange(int offsetInMaster, int lengthInMaster, DocumentEvent masterDocumentEvent) throws BadLocationException {
		
		if (lengthInMaster == 0)
			return;
		
		try {
			
			Position[] fragments= getFragments();
			int index= fMasterDocument.computeIndexInCategory(fFragmentsCategory, offsetInMaster);
			
			Fragment left= null;
			Fragment right= null;
			
			if (index < fragments.length) {
				if (offsetInMaster == fragments[index].offset)
					throw new IllegalArgumentException("overlaps with existing fragment"); //$NON-NLS-1$
				if (offsetInMaster + lengthInMaster == fragments[index].offset) 
					right= (Fragment) fragments[index];
			}
			
			if (0 < index && index <= fragments.length) {
				Fragment fragment= (Fragment) fragments[index - 1];
				if (fragment.includes(offsetInMaster))
					throw new IllegalArgumentException("overlaps with existing fragment"); //$NON-NLS-1$
				if (fragment.getOffset() + fragment.getLength() == offsetInMaster)
					left= fragment;
			}
			
			int offsetInSlave= 0;
			if (index > 0) {
				Fragment fragment= (Fragment) fragments[index - 1];
				Segment segment= fragment.segment;
				offsetInSlave= segment.getOffset() + segment.getLength();
			}
			
			ProjectionDocumentEvent event= new ProjectionDocumentEvent(this, offsetInSlave, 0, fMasterDocument.get(offsetInMaster, lengthInMaster), offsetInMaster, lengthInMaster, masterDocumentEvent);
			super.fireDocumentAboutToBeChanged(event);

			// check for neighboring fragment
			if (left != null && right != null) {
				
				int endOffset= right.getOffset() + right.getLength();
				left.setLength(endOffset - left.getOffset());
				left.segment.setLength(left.segment.getLength() + right.segment.getLength());
				
				removePosition(fSegmentsCategory, right.segment);
				fMasterDocument.removePosition(fFragmentsCategory, right);
				
			} else if (left != null) {
				int endOffset= offsetInMaster +lengthInMaster;
				left.setLength(endOffset - left.getOffset());
				left.segment.markForStretch();
				
			} else if (right != null) {
				right.setOffset(right.getOffset() - lengthInMaster);
				right.setLength(right.getLength() + lengthInMaster);
				right.segment.markForStretch();
				
			} else {
				// create a new segment
				Fragment fragment= new Fragment(offsetInMaster, lengthInMaster);
				fMasterDocument.addPosition(fFragmentsCategory, fragment);
				Segment segment= createSegmentFor(fragment, index);
				segment.markForStretch();
			}
			
			getTracker().replace(event.getOffset(), event.getLength(), event.getText());
			super.fireDocumentChanged(event);
			
		} catch (BadPositionCategoryException x) {
			internalError();
		}
	}
	
	/**
	 * Finds the fragment of the master document that represents the given range.
	 * 
	 * @param offsetInMaster the offset of the range in the master document
	 * @param lengthInMaster the length of the range in the master document
	 * @return the fragment representing the given master document range
	 */
	private Fragment findFragment(int offsetInMaster, int lengthInMaster) {
		Position[] fragments= getFragments();
		for (int i= 0; i < fragments.length; i++) {
			Fragment f= (Fragment) fragments[i];
			if (f.getOffset() <= offsetInMaster && offsetInMaster + lengthInMaster <= f.getOffset() + f.getLength())
				return f;
		}
		return null;
	}
	
	/**
	 * Removes the given range of the master document from this projection
	 * document.
	 * 
	 * @param offsetInMaster the offset of the range in the master document
	 * @param lengthInMaster the length of the range in the master document
	 * 
	 * @throws BadLocationException if the given range is not valid in the
	 *             master document
	 * @throws IllegalArgumentException if the given range is not projected in
	 *             this projection document or is not completely comprised by
	 *             an existing fragment
	 */
	private void internalRemoveMasterDocumentRange(int offsetInMaster, int lengthInMaster) throws BadLocationException {
		try {
			
			IRegion imageRegion= fMapping.toExactImageRegion(new Region(offsetInMaster, lengthInMaster));
			if (imageRegion == null)
				throw new IllegalArgumentException();
			
			Fragment fragment= findFragment(offsetInMaster, lengthInMaster);
			if (fragment == null)
				throw new IllegalArgumentException();
			
			ProjectionDocumentEvent event= new ProjectionDocumentEvent(this, imageRegion.getOffset(), imageRegion.getLength(), null, offsetInMaster, lengthInMaster);
			super.fireDocumentAboutToBeChanged(event);
			
			if (fragment.getOffset() == offsetInMaster) {
				fragment.setOffset(offsetInMaster + lengthInMaster);
				fragment.setLength(fragment.getLength() - lengthInMaster);
			} else if (fragment.getOffset() + fragment.getLength() == offsetInMaster + lengthInMaster) {
				fragment.setLength(fragment.getLength() - lengthInMaster);
			} else {
				// split fragment into three fragments, let position updater remove it
				
				// add fragment for the region to be removed
				Fragment newFragment= new Fragment(offsetInMaster, lengthInMaster);
				Segment segment= new Segment(imageRegion.getOffset(), imageRegion.getLength());
				newFragment.segment= segment;
				segment.fragment= newFragment;
				fMasterDocument.addPosition(fFragmentsCategory, newFragment);
				addPosition(fSegmentsCategory, segment);
				
				// add fragment for the remainder right of the deleted range in the original fragment
				int offset= offsetInMaster + lengthInMaster;
				newFragment= new Fragment(offset, fragment.getOffset() + fragment.getLength() - offset);
				offset= imageRegion.getOffset() + imageRegion.getLength();
				segment= new Segment(offset, fragment.segment.getOffset() + fragment.segment.getLength() - offset);
				newFragment.segment= segment;
				segment.fragment= newFragment;
				fMasterDocument.addPosition(fFragmentsCategory, newFragment);
				addPosition(fSegmentsCategory, segment);
				
				// adjust length of initial fragment (the left one)
				fragment.setLength(offsetInMaster - fragment.getOffset());
				fragment.segment.setLength(imageRegion.getOffset() - fragment.segment.getOffset());
			}
			
			getTracker().replace(event.getOffset(), event.getLength(), event.getText());
			super.fireDocumentChanged(event);
			
		} catch (BadPositionCategoryException x) {
			internalError();
		}
	}
	
	/**
	 * Returns the sequence of all master document regions with are contained in the given master document
	 * range and which are not yet part of this projection document.
	 * 
	 * @param offsetInMaster the range offset in the master document
	 * @param lengthInMaster the range length in the master document
	 * @return the sequence of regions which are not yet part of the projection document
	 * @throws BadLocationException in case the given range is invalid in the master document
	 */
	public final IRegion[] computeUnprojectedMasterRegions(int offsetInMaster, int lengthInMaster) throws BadLocationException {
		
		IRegion[] fragments= null;
		IRegion imageRegion= fMapping.toImageRegion(new Region(offsetInMaster, lengthInMaster));
		if (imageRegion != null)
			fragments= fMapping.toExactOriginRegions(imageRegion);
		
		if (fragments == null || fragments.length == 0)
			return new IRegion[] { new Region(offsetInMaster, lengthInMaster) };
		
		List gaps= new ArrayList();
		
		IRegion region= fragments[0];
		if (offsetInMaster < region.getOffset())
			gaps.add(new Region(offsetInMaster, region.getOffset() - offsetInMaster));
		
		for (int i= 0; i < fragments.length - 1; i++) {
			IRegion left= fragments[i];
			IRegion right= fragments[i + 1];
			int leftEnd= left.getOffset() + left.getLength();
			if (leftEnd < right.getOffset())
				gaps.add(new Region(leftEnd, right.getOffset() - leftEnd));
		}
		
		region= fragments[fragments.length - 1];
		int leftEnd= region.getOffset() + region.getLength();
		int rightEnd= offsetInMaster + lengthInMaster;
		if (leftEnd < rightEnd)
			gaps.add(new Region(leftEnd, rightEnd - leftEnd));
		
		IRegion[] result= new IRegion[gaps.size()];
		gaps.toArray(result);
		return result;
	}
	
	/**
	 * Ensures that the given range of the master document is part of this
	 * projection document.
	 * 
	 * @param offsetInMaster the offset of the master document range
	 * @param lengthInMaster the length of the master document range
	 * @throws BadLocationException in case the master event is not valid
	 */
	public void addMasterDocumentRange(int offsetInMaster, int lengthInMaster) throws BadLocationException {
		addMasterDocumentRange(offsetInMaster, lengthInMaster, null);
	}

	/**
	 * Ensures that the given range of the master document is part of this
	 * projection document.
	 * 
	 * @param offsetInMaster the offset of the master document range
	 * @param lengthInMaster the length of the master document range
	 * @param masterDocumentEvent the master document event which causes this
	 *            projection change, or <code>null</code> if none
	 * @throws BadLocationException in case the master event is not valid
	 */
	private void addMasterDocumentRange(int offsetInMaster, int lengthInMaster, DocumentEvent masterDocumentEvent) throws BadLocationException {
		
		IRegion[] gaps= computeUnprojectedMasterRegions(offsetInMaster, lengthInMaster);
		if (gaps == null)
			return;
		
		for (int i= 0; i < gaps.length; i++) {
			IRegion gap= gaps[i];
			internalAddMasterDocumentRange(gap.getOffset(), gap.getLength(), masterDocumentEvent);
		}
	}
	
	/**
	 * Ensures that the given range of the master document is not part of this
	 * projection document.
	 * 
	 * @param offsetInMaster the offset of the master document range
	 * @param lengthInMaster the length of the master document range
	 * @throws BadLocationException in case the master event is not valid
	 */
	public void removeMasterDocumentRange(int offsetInMaster, int lengthInMaster) throws BadLocationException {
		IRegion[] fragments= computeProjectedMasterRegions(offsetInMaster, lengthInMaster);
		if (fragments == null || fragments.length == 0)
			return;
		
		for (int i= 0; i < fragments.length; i++) {
			IRegion fragment= fragments[i];
			internalRemoveMasterDocumentRange(fragment.getOffset(), fragment.getLength());
		}
	}
	
	/**
	 * Returns the sequence of all master document regions with are contained in the given master document
	 * range and which are part of this projection document. May return <code>null</code> if no such
	 * regions exist.
	 * 
	 * @param offsetInMaster the range offset in the master document
	 * @param lengthInMaster the range length in the master document
	 * @return the sequence of regions which are part of the projection document or <code>null</code>
	 * @throws BadLocationException in case the given range is invalid in the master document
	 */
	final public IRegion[] computeProjectedMasterRegions(int offsetInMaster, int lengthInMaster) throws BadLocationException {
		IRegion imageRegion= fMapping.toImageRegion(new Region(offsetInMaster, lengthInMaster));
		if (imageRegion != null)
			return fMapping.toExactOriginRegions(imageRegion);
		return null;
	}
	
	/**
	 * Returns whether this project is being updated.
	 * 
	 * @return <code>true</code> if the document is updating
	 */
	protected boolean isUpdating() {
		return fIsUpdating;
	}
	
	/*
	 * @see org.eclipse.jface.text.IDocument#replace(int, int, java.lang.String)
	 */
	public void replace(int offset, int length, String text) throws BadLocationException {
		try {
			fIsUpdating= true;
			if (fMasterDocumentExtension != null)
				fMasterDocumentExtension.stopPostNotificationProcessing();
				
			super.replace(offset, length, text);
			
		} finally {
			fIsUpdating= false;
			if (fMasterDocumentExtension != null)
				fMasterDocumentExtension.resumePostNotificationProcessing();
		}
	}
	
	/*
	 * @see org.eclipse.jface.text.IDocument#set(java.lang.String)
	 */
	public void set(String text) {
		try {
			fIsUpdating= true;
			if (fMasterDocumentExtension != null)
				fMasterDocumentExtension.stopPostNotificationProcessing();
				
			super.set(text);
		
		} finally {
			fIsUpdating= false;
			if (fMasterDocumentExtension != null)
				fMasterDocumentExtension.resumePostNotificationProcessing();
		}
	}
	
	/**
	 * Transforms a document event of the master document into a projection
	 * document based document event.
	 * 
	 * @param masterEvent the master document event
	 * @return the slave document event
	 * @throws BadLocationException in case the master event is not valid
	 */
	private ProjectionDocumentEvent normalize(DocumentEvent masterEvent) throws BadLocationException {
		if (!isUpdating()) {
			IRegion imageRegion= fMapping.toExactImageRegion(new Region(masterEvent.getOffset(), masterEvent.getLength()));
			if (imageRegion != null)
				return new ProjectionDocumentEvent(this, imageRegion.getOffset(), imageRegion.getLength(), masterEvent.getText(), masterEvent);
			return null;
		} else {
			ProjectionDocumentEvent event= new ProjectionDocumentEvent(this, fOriginalEvent.getOffset(), fOriginalEvent.getLength(), fOriginalEvent.getText(), masterEvent);
			fOriginalEvent= null;
			return event;
		}
	}
	
	private boolean includes(IRegion region, int offset) {
		if (region == null)
			return false;
		return region.getOffset() <= offset && offset <= region.getOffset() + region.getLength();
	}
	
	private boolean includes(IRegion region1, int offset, int length) {
		if (region1 == null)
			return false;
		return region1.getOffset() <= offset && (offset + length <= region1.getOffset() + region1.getLength());
	}
	
	/**
	 * Compute the gaps that must be covered in order to ensure that the range affected
	 * by the given document event is completely projected.
	 * 
	 * @param event the master document event
	 * @return the gaps to be covered
	 */
	private IRegion[] computeCoverageGap(DocumentEvent event) {
		
		IRegion left= null;
		List gaps= new ArrayList();
		
		try {
			// deal with the beginning of the event region
			int imageOffset= fMapping.toImageOffset(event.getOffset());
			if (imageOffset == -1) {
				Position[] fragments= getFragments();
				int index= fMasterDocument.computeIndexInCategory(fFragmentsCategory, event.getOffset());
				if (index < fragments.length) {
					Fragment fragment= (Fragment) fragments[index];
					left= new Region(event.getOffset(), fragment.getOffset() - event.getOffset());
					gaps.add(left);
				}
			}
			
			// the event itself
			if (!includes(left, event.getOffset(), event.getLength()))
				gaps.add(new Region(event.getOffset(), event.getLength()));
			
			// deal with the end of the event region
			int inclusiveOriginEndOffset= event.getOffset() + Math.max(0, event.getLength() - 1);
			int inclusiveImageEndOffset= fMapping.toImageOffset(inclusiveOriginEndOffset);
			if (inclusiveImageEndOffset == -1 && !includes(left, inclusiveOriginEndOffset)) {
				int index= fMasterDocument.computeIndexInCategory(fFragmentsCategory, inclusiveOriginEndOffset);
				if (0 < index) {
					Position[] fragments= getFragments();
					Fragment fragment= (Fragment) fragments[index - 1];	
					gaps.add(new Region(fragment.getOffset(), inclusiveOriginEndOffset + 1 - fragment.getOffset()));
				}
			}
			
		} catch (BadLocationException e) {
			internalError();
		} catch (BadPositionCategoryException e) {
			internalError();
		}
		
		IRegion[] result= new IRegion[gaps.size()];
		gaps.toArray(result);
		return result;
	}
	
	/**
	 * Ensures that when the master event effects this projection document, that the whole region described by the
	 * event is part of this projection document.
	 * 
	 * @param masterEvent the master document event
	 * @return <code>true</code> if masterEvent affects this projection document
	 * @throws BadLocationException in case the master event is not valid
	 */
	protected final boolean adaptProjectionToMasterChange(DocumentEvent masterEvent) throws BadLocationException {
		if (!isUpdating() && fFragmentsUpdater.affectsPositions(masterEvent) || fIsAutoExpanding) {
			
			IRegion[] gaps= computeCoverageGap(masterEvent);
			for (int i= 0; i < gaps.length; i++) {
				IRegion gap= gaps[i];
				addMasterDocumentRange(gap.getOffset(), gap.getLength(), masterEvent);
			}
			return true;				
		
		} else if (fMapping.getImageLength() == 0 && masterEvent.getLength() == 0) {
			
			Position[] fragments= getFragments();
			if (fragments.length == 0) {
				// there is no segment in this projection document, thus one must be created
				// need to bypass the usual infrastructure as the new segment/fragment would be of length 0 and thus the segmentation be not well formed
				try {
					Fragment fragment= new Fragment(0, 0);
					fMasterDocument.addPosition(fFragmentsCategory, fragment);
					createSegmentFor(fragment, 0);
				} catch (BadPositionCategoryException x) {
					internalError();
				}
			}
		}
		
		return isUpdating();
	}
	
	/**
	 * When called, this projection document is informed about a forthcoming
	 * change of its master document. This projection document checks whether
	 * the master document change affects it and if so informs all document
	 * listeners.
	 * 
	 * @param masterEvent the master document event
	 */
	public void masterDocumentAboutToBeChanged(DocumentEvent masterEvent) {
		try {
			
			boolean assertNotNull= adaptProjectionToMasterChange(masterEvent);
			fSlaveEvent= normalize(masterEvent);
			if (assertNotNull && fSlaveEvent == null)
				internalError();
			
			fMasterEvent= masterEvent;
			if (fSlaveEvent != null)
				delayedFireDocumentAboutToBeChanged();
		
		} catch (BadLocationException e) {
			internalError();
		}
	}
	
	/**
	 * When called, this projection document is informed about a change of its
	 * master document. If this projection document is affected it informs all
	 * of its document listeners.
	 * 
	 * @param masterEvent the master document event
	 */
	public void masterDocumentChanged(DocumentEvent masterEvent) {
		if ( !isUpdating() && masterEvent == fMasterEvent) {
			if (fSlaveEvent != null) {
				try {
					getTracker().replace(fSlaveEvent.getOffset(), fSlaveEvent.getLength(), fSlaveEvent.getText());
					fireDocumentChanged(fSlaveEvent);
				} catch (BadLocationException e) {
					internalError();
				}
			} else if (ensureWellFormedSegmentation(masterEvent.getOffset()))
				fMapping.projectionChanged();
		}
	}
	
	/*
	 * @see org.eclipse.jface.text.AbstractDocument#fireDocumentAboutToBeChanged(org.eclipse.jface.text.DocumentEvent)
	 */
	protected void fireDocumentAboutToBeChanged(DocumentEvent event) {
		fOriginalEvent= event;
		// delay it until there is a notification from the master document
		// at this point, it is expensive to construct the master document information
	}
	
	/**
	 * Fires the slave document event as about-to-be-changed event to all registered listeners.
	 */
	private void delayedFireDocumentAboutToBeChanged() {
		super.fireDocumentAboutToBeChanged(fSlaveEvent);
	}
	
	/**
	 * Ignores the given event and sends the semantically equal slave document event instead.
	 *
	 * @param event the event to be ignored
	 */
	protected void fireDocumentChanged(DocumentEvent event) {
		super.fireDocumentChanged(fSlaveEvent);
	}
	
	/*
	 * @see org.eclipse.jface.text.AbstractDocument#updateDocumentStructures(org.eclipse.jface.text.DocumentEvent)
	 */
	protected void updateDocumentStructures(DocumentEvent event) {
		super.updateDocumentStructures(event);
		ensureWellFormedSegmentation(computeAnchor(event));
		fMapping.projectionChanged();
	}
	
	private int computeAnchor(DocumentEvent event) {
		if (event instanceof ProjectionDocumentEvent) {
			ProjectionDocumentEvent slave= (ProjectionDocumentEvent) event;
			if (ProjectionDocumentEvent.CONTENT_CHANGE == slave.getChangeType()) {
				DocumentEvent master= slave.getMasterEvent();
				if (master != null)
					return master.getOffset();
			}
		}
		return -1;
	}
	
	private boolean ensureWellFormedSegmentation(int anchorOffset) {
		boolean changed= false;
		Position[] segments= getSegments();
		for (int i= 0; i < segments.length; i++) {
			Segment segment= (Segment) segments[i];
			if (segment.isDeleted() || segment.getLength() == 0) {
				try {
					removePosition(fSegmentsCategory, segment); 
					fMasterDocument.removePosition(fFragmentsCategory, segment.fragment);
					changed= true;
				} catch (BadPositionCategoryException e) {
					internalError();
				}
			} else if (i < segments.length - 1) {
				Segment next= (Segment) segments[i + 1];
				if (next.isDeleted() || next.getLength() == 0)
					continue;
				Fragment fragment= segment.fragment;
				if (fragment.getOffset() + fragment.getLength() == next.fragment.getOffset()) {
					// join fragments and their corresponding segments
					segment.setLength(segment.getLength() + next.getLength());
					fragment.setLength(fragment.getLength() + next.fragment.getLength());
					next.delete();
				}
			}
		}
		
		if (changed && anchorOffset != -1) {
			Position[] changedSegments= getSegments();
			if (changedSegments == null || changedSegments.length == 0) {
				Fragment fragment= new Fragment(anchorOffset, 0);
				try {
					fMasterDocument.addPosition(fFragmentsCategory, fragment);
					createSegmentFor(fragment, 0);
				} catch (BadLocationException e) {
					internalError();
				} catch (BadPositionCategoryException e) {
					internalError();
				}
			}
		}
		
		return changed;
	}

	/*
	 * @see IDocumentExtension#registerPostNotificationReplace(IDocumentListener, IDocumentExtension.IReplace)
	 */
	public void registerPostNotificationReplace(IDocumentListener owner, IDocumentExtension.IReplace replace) {
		if (!isUpdating())
			throw new UnsupportedOperationException();
		super.registerPostNotificationReplace(owner, replace);
	}
	
	/** 
	 * Sets the auto expand mode for this document.
	 * 
	 * @param autoExpandMode <code>true</code> if auto-expanding
	 */
	public void setAutoExpandMode(boolean autoExpandMode) {
		fIsAutoExpanding= autoExpandMode;
	}
	
	/**
	 * Replaces all master document ranges with the given master document range.
	 * 
	 * @param offsetInMaster the offset in the master document
	 * @param lengthInMaster the length in the master document
	 * @throws BadLocationException if the given range of the master document is not valid
	 */
	public void replaceMasterDocumentRanges(int offsetInMaster, int lengthInMaster) throws BadLocationException {
		try {
			
			ProjectionDocumentEvent event= new ProjectionDocumentEvent(this, 0, fMapping.getImageLength(), fMasterDocument.get(offsetInMaster, lengthInMaster), offsetInMaster, lengthInMaster);
			super.fireDocumentAboutToBeChanged(event);
			
			Position[] fragments= getFragments();
			for (int i= 0; i < fragments.length; i++) {
				Fragment fragment= (Fragment) fragments[i];
				fMasterDocument.removePosition(fFragmentsCategory, fragment);
				removePosition(fSegmentsCategory, fragment.segment);
			}
			
			Fragment fragment= new Fragment(offsetInMaster, lengthInMaster);
			Segment segment= new Segment(0, 0);
			segment.fragment= fragment;
			fragment.segment= segment;
			fMasterDocument.addPosition(fFragmentsCategory, fragment);
			addPosition(fSegmentsCategory, segment);
			
			getTracker().set(fMasterDocument.get(offsetInMaster, lengthInMaster));
			super.fireDocumentChanged(event);
			
		} catch (BadPositionCategoryException x) {
			internalError();
		}
	}
}

Back to the top