Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: 4827b11f3b26c4ea4db1d113d4f6fb823af42006 (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
/**********************************************************************
Copyright (c) 2000, 2002 IBM Corp. 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 implementation
**********************************************************************/

package org.eclipse.jface.text.formatter;


import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.jface.text.Assert;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DefaultPositionUpdater;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IPositionUpdater;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.TypedPosition;


/**
 * Standard implementation of <code>IContentFormatter</code>.
 * The formatter supports two operation modi: partition aware and
 * partition unaware. <p>
 * In the partition aware mode, the formatter determines the 
 * partitioning of the document region to be formatted. For each 
 * partition it determines all document positions  which are affected 
 * when text changes are applied to the partition. Those which overlap
 * with the partition are remembered as character positions. These
 * character positions are passed over to the formatting strategy
 * registered for the partition's content type. The formatting strategy
 * returns a string containing the formatted document partition as well
 * as the adapted character positions. The formatted partition replaces
 * the old content of the partition. The remembered document postions 
 * are updated with the adapted character positions. In addition, all
 * other document positions are accordingly adapted to the formatting 
 * changes.<p>
 * In the partition unaware mode, the document's partitioning is ignored
 * and the document is considered consisting of only one partition of 
 * the content type <code>IDocument.DEFAULT_CONTENT_TYPE</code>. The 
 * formatting process is similar to the partition aware mode, with the 
 * exception of having only one partition.<p>
 * Usually, clients instantiate this class and configure it before using it.
 *
 * @see IContentFormatter
 * @see IDocument
 * @see ITypedRegion
 * @see Position
 */
public class ContentFormatter implements IContentFormatter {
		
	/**
	 * Defines a reference to either the offset or the end offset of
	 * a particular position.
	 */
	static class PositionReference implements Comparable {
		
		/** The referenced position */
		protected Position fPosition;
		/** The reference to either the offset or the end offset */
		protected boolean fRefersToOffset;
		/** The original category of the referenced position */
		protected String fCategory;
		
		protected PositionReference(Position position, boolean refersToOffset, String category) {
			fPosition= position;
			fRefersToOffset= refersToOffset;
			fCategory= category;
		}
		
		/**
		 * Returns the offset of the referenced position.
		 */
		protected int getOffset() {
			return fPosition.getOffset();
		}
		
		/**
		 * Manipulates the offset of the referenced position.
		 */
		protected void setOffset(int offset) {
			fPosition.setOffset(offset);
		}
		
		/**
		 * Returns the length of the referenced position.
		 */
		protected int getLength() {
			return fPosition.getLength();
		}
		
		/**
		 * Manipulates the length of the referenced position.
		 */
		protected void setLength(int length) {
			fPosition.setLength(length);
		}
		
		/**
		 * Returns whether this reference points to the offset or endoffset
		 * of the references position.
		 */
		protected boolean refersToOffset() {
			return fRefersToOffset;
		}
		
		/**
		 * Returns the category of the referenced position.
		 */
		protected String getCategory() {
			return fCategory;
		}
		
		/**
		 * Returns the referenced position.
		 */
		protected Position getPosition() {
			return fPosition;
		}
		
		/**
		 * Returns the referenced character position
		 */
		protected int getCharacterPosition() {
			if (fRefersToOffset)
				return getOffset();
			return getOffset() + getLength();
		}
		
		/**
		 * @see Comparable#compareTo(Object)
		 */
		public int compareTo(Object obj) {
			
			if (obj instanceof PositionReference) {
				PositionReference r= (PositionReference) obj;
				return getCharacterPosition() - r.getCharacterPosition();
			}
			
			throw new ClassCastException();
		}
	};
	
	/**
	 * The position updater used to adapt all to update the 
	 * remembered partitions.
	 *
	 * @see IPositionUpdater
	 * @see DefaultPositionUpdater
	 */
	class NonDeletingPositionUpdater extends DefaultPositionUpdater {
		
		protected NonDeletingPositionUpdater(String category) {
			super(category);
		}
		
		/*
		 * @see DefaultPositionUpdater#notDeleted()
		 */
		protected boolean notDeleted() {
			return true;
		}
	};
	
	/**
	 * The position updater which runs as first updater on the document's positions.
	 * Used to remove all affected positions from their categories to avoid them
	 * from being regularily updated.
	 * 
	 * @see IPositionUpdater
	 */
	class RemoveAffectedPositions implements IPositionUpdater {
		/**
		 * @see IPositionUpdater#update(DocumentEvent)
		 */
		public void update(DocumentEvent event) {
			removeAffectedPositions(event.getDocument());
		}	
	};
	
	/**
	 * The position updater which runs as last updater on the document's positions.
	 * Used to update all affected positions and adding them back to their
	 * original categories.
	 * 
	 * @see IPositionUpdater
	 */
	class UpdateAffectedPositions implements IPositionUpdater {
		
		private int[] fPositions;
		private int fOffset;
		
		public UpdateAffectedPositions(int[] positions, int offset) {
			fPositions= positions;
			fOffset= offset;
		}
		
		/**
		 * @see IPositionUpdater#update(DocumentEvent)
		 */
		public void update(DocumentEvent event) {
			updateAffectedPositions(event.getDocument(), fPositions, fOffset);
		}	
	};
	
	
	/** Internal position category used for the formatter partitioning */
	private final static String PARTITIONING= "__formatter_partitioning"; //$NON-NLS-1$
	
	/** The map of <code>IFormattingStrategy</code> objects */
	private Map fStrategies;
	/** The indicator of whether the formatter operates in partition aware mode or not */
	private boolean fIsPartitionAware= true;
	
	/** The partition information managing document position categories */
	private String[] fPartitionManagingCategories;
	/** The list of references to offset and end offset of all overlapping positions */
	private List fOverlappingPositionReferences;
	/** Position updater used for partitioning positions */
	private IPositionUpdater fPartitioningUpdater;
	
	
	
	/**
	 * Creates a new content formatter. The content formatter operates by default
	 * in the partition-aware mode. There are no preconfigured formatting strategies.
	 */
	public ContentFormatter() {
	}
		
	/**
	 * Registers a strategy for a particular content type. If there is already a strategy
	 * registered for this type, the new strategy is registered instead of the old one.
	 * If the given content type is <code>null</code> the given strategy is registered for
	 * all content types as is called only once per formatting session.
	 *
	 * @param strategy the formatting strategy to register, or <code>null</code> to remove an existing one
	 * @param contentType the content type under which to register, or <code>null</code> for all content types
	 */
	public void setFormattingStrategy(IFormattingStrategy strategy, String contentType) {
		
		Assert.isNotNull(contentType);
			
		if (fStrategies == null)
			fStrategies= new HashMap();
			
		if (strategy == null)
			fStrategies.remove(contentType);
		else
			fStrategies.put(contentType, strategy);
	}
	
	/**
	 * Informs this content formatter about the names of those position categories
	 * which are used to manage the document's partitioning information and thus should
	 * be ignored when this formatter updates positions.
	 *
	 * @param categories the categories to be ignored
	 */
	public void setPartitionManagingPositionCategories(String[] categories) {
		fPartitionManagingCategories= categories;
	}
	
	/**
	 * Sets the formatter's operation mode.
	 * 
	 * @param enable indicates whether the formatting process should be partition ware
	 */
	public void enablePartitionAwareFormatting(boolean enable) {
		fIsPartitionAware= enable;
	}
	
	/*
	 * @see IContentFormatter#getFormattingStrategy
	 */
	public IFormattingStrategy getFormattingStrategy(String contentType) {
		
		Assert.isNotNull(contentType);
		
		if (fStrategies == null)
			return null;
								
		return (IFormattingStrategy) fStrategies.get(contentType);
	}
	
	/*
	 * @see IContentFormatter#format
	 */
	public void format(IDocument document, IRegion region) {
		if (fIsPartitionAware)
			formatPartitions(document, region);
		else
			formatRegion(document, region);
	}
		
	/**
	 * Determines the partitioning of the given region of the document.
	 * Informs for each partition about the start, the process, and the
	 * termination of the formatting session.
	 */
	private void formatPartitions(IDocument document, IRegion region) {
		
		addPartitioningUpdater(document);
		
		try {
			
			TypedPosition[] ranges= getPartitioning(document, region);
			if (ranges != null) {
				start(ranges, getIndentation(document, region.getOffset()));
				format(document, ranges);
				stop(ranges);
			}
			
		} catch (BadLocationException x) {
		}
			
		removePartitioningUpdater(document);
	}
	
	/**
	 * Informs for the given region about the start, the process, and
	 * the termination of the formatting session.
	 */
	private void formatRegion(IDocument document, IRegion region) {
		
		IFormattingStrategy strategy= getFormattingStrategy(IDocument.DEFAULT_CONTENT_TYPE);
		if (strategy != null) {
			strategy.formatterStarts(getIndentation(document, region.getOffset()));
			format(document, strategy, new TypedPosition(region.getOffset(), region.getLength(), IDocument.DEFAULT_CONTENT_TYPE));
			strategy.formatterStops();
		}
	}
	
	/**
	 * Returns the partitioning of the given region of the specified document.
	 * As one partition after the other will be formatted and formatting will 
	 * probably change the length of the formatted partition, it must be kept 
	 * track of the modifications in order to submit the correct partition to all 
	 * formatting strategies. For this, all partitions are remembered as positions
	 * in a dedicated position category. (As formatting stratgies might rely on each
	 * other, calling them in reversed order is not an option.)
	 *
	 * @param document the document
	 * @param region the region for which the partitioning must be determined
	 * @return the partitioning of the specified region
	 * @exception BadLocationException of region is invalid in the document
	 */
	private TypedPosition[] getPartitioning(IDocument document, IRegion region) throws BadLocationException {
		
		ITypedRegion[] regions= document.computePartitioning(region.getOffset(), region.getLength());
		TypedPosition[] positions= new TypedPosition[regions.length];
		
		for (int i= 0; i < regions.length; i++) {
			positions[i]= new TypedPosition(regions[i]);
			try {
				document.addPosition(PARTITIONING, positions[i]);
			} catch (BadPositionCategoryException x) {
				// should not happen
			}
		}
		
		return positions;
	}
	
	/**
	 * Fires <code>formatterStarts</code> to all formatter strategies
	 * which will be involved in the forthcoming formatting process.
	 * 
	 * @param regions the partitioning of the document to be formatted
	 * @param indentation the initial indentation
	 */
	private void start(TypedPosition[] regions, String indentation) {
		for (int i= 0; i < regions.length; i++) {
			IFormattingStrategy s= getFormattingStrategy(regions[i].getType());
			if (s != null)
				s.formatterStarts(indentation);
		}
	}
	
	/**
	 * Formats one partition after the other using the formatter strategy registered for
	 * the partition's content type.
	 *
	 * @param document to document to be formatted
	 * @param ranges the partitioning of the document region to be formatted
	 */
	private void format(final IDocument document, TypedPosition[] ranges) {
		for (int i= 0; i < ranges.length; i++) {
			IFormattingStrategy s= getFormattingStrategy(ranges[i].getType());
			if (s != null) {
				format(document, s, ranges[i]);
			}
		}
	}
	
	/**
	 * Formats the given region of the document using the specified formatting
	 * strategy. In order to maintain positions correctly, first all affected 
	 * positions determined, after all document listeners have been informed about
	 * the upcoming change, the affected positions are removed to avoid that they
	 * are regularily updated. After all position updaters have run, the affected
	 * positions are updated with the formatter's information and added back to 
	 * their categories, right before the first document listener is informed about
	 * that a change happend.
	 * 
	 * @param document the document to be formatted
	 * @param strategy the strategy to be used
	 * @param region the region to be formatted
	 */
	private void format(final IDocument document, IFormattingStrategy strategy, TypedPosition region) {
		try {
		
			final int offset= region.getOffset();
			int length= region.getLength();
		
			String content= document.get(offset, length);
			final int[] positions= getAffectedPositions(document, offset, length);
			String formatted= strategy.format(content, isLineStart(document, offset), getIndentation(document, offset), positions);
			
			IPositionUpdater first= new RemoveAffectedPositions();
			document.insertPositionUpdater(first, 0);
			IPositionUpdater last= new UpdateAffectedPositions(positions, offset);
			document.addPositionUpdater(last);
			
			document.replace(offset, length, formatted);
			
			document.removePositionUpdater(first);
			document.removePositionUpdater(last);
					
		} catch (BadLocationException x) {
			// should not happen
		}
	}
	
	/**
	 * Fires <code>formatterStops</code> to all formatter strategies which were
	 * involved in the formatting process which is about to terminate.
	 *
	 * @param regions the partitioning of the document which has been formatted
	 */
	private void stop(TypedPosition[] regions) {
		for (int i= 0; i < regions.length; i++) {
			IFormattingStrategy s= getFormattingStrategy(regions[i].getType());
			if (s != null)
				s.formatterStops();
		}
	}
		
	/**
	 * Installs those updaters which the formatter needs to keep 
	 * track of the partitions.
	 *
	 * @param document the document to be formatted
	 */
	private void addPartitioningUpdater(IDocument document) {
		fPartitioningUpdater= new NonDeletingPositionUpdater(PARTITIONING);
		document.addPositionCategory(PARTITIONING);
		document.addPositionUpdater(fPartitioningUpdater);
	}
	
	/**
	 * Removes the formatter's internal position updater and category.
	 *
	 * @param document the document that has been formatted
	 */
	private void removePartitioningUpdater(IDocument document) {
		
		try {
						
			document.removePositionUpdater(fPartitioningUpdater);
			document.removePositionCategory(PARTITIONING);
			fPartitioningUpdater= null;
			
		} catch (BadPositionCategoryException x) {
			// should not happen
		}
	}
	
	/**
	 * Determines whether the given document position category should be ignored
	 * by this formatter's position updating.
	 *
	 * @param category the category to check
	 * @return <code>true</code> if the category should be ignored, <code>false</code> otherwise
	 */
	private boolean ignoreCategory(String category) {
		
		if (PARTITIONING.equals(category))
			return true;
						
		if (fPartitionManagingCategories != null) {
			for (int i= 0; i < fPartitionManagingCategories.length; i++) {
				if (fPartitionManagingCategories[i].equals(category))
					return true;
			}
		}
		
		return false;
	}
	
	/**
	 * Determines all embracing, overlapping, and follow up positions 
	 * for the given region of the document.
	 *
	 * @param document the document to be formatted
	 * @param offset the offset of the document region to be formatted
	 * @param length the length of the document to be formatted
	 */
	private void determinePositionsToUpdate(IDocument document, int offset, int length) {
		
		String[] categories= document.getPositionCategories();
		if (categories != null) {
			for (int i= 0; i < categories.length; i++) {
				
				if (ignoreCategory(categories[i]))
					continue;
					
				try {
					
					Position[] positions= document.getPositions(categories[i]);
					
					for (int j= 0; j < positions.length; j++) {
						
						Position p= (Position) positions[j];
						if (p.overlapsWith(offset, length)) {
							
							if (offset < p.getOffset())
								fOverlappingPositionReferences.add(new PositionReference(p, true, categories[i]));
							
							if (p.getOffset() + p.getLength() < offset + length)
								fOverlappingPositionReferences.add(new PositionReference(p, false, categories[i]));
						}
					}
					
				} catch (BadPositionCategoryException x) {
					// can not happen
				}
			}
		}
	}
	
	/**
	 * Returns all offset and the end offset of all positions overlapping with the 
	 * specified document range.
	 *
	 * @param document the document to be formatted
	 * @param offset the offset of the document region to be formatted
	 * @param length the length of the document to be formatted
	 * @return all character positions of the interleaving positions
	 */
	private int[] getAffectedPositions(IDocument document, int offset, int length) {
		
		fOverlappingPositionReferences= new ArrayList();
		
		determinePositionsToUpdate(document, offset, length);
		
		Collections.sort(fOverlappingPositionReferences);
		
		int[] positions= new int[fOverlappingPositionReferences.size()];
		for (int i= 0; i < positions.length; i++) {
			PositionReference r= (PositionReference) fOverlappingPositionReferences.get(i);
			positions[i]= r.getCharacterPosition() - offset;
		}
		
		return positions;
	}
	
	/**
	 * Removes the affected positions from their categories to avoid
	 * that they are invalidly updated.
	 * 
	 * @param document the document 
	 */
	private void removeAffectedPositions(IDocument document) {
		int size= fOverlappingPositionReferences.size();
		for (int i= 0; i < size; i++) {
			PositionReference r= (PositionReference) fOverlappingPositionReferences.get(i);
			try {
				document.removePosition(r.getCategory(), r.getPosition());
			} catch (BadPositionCategoryException x) {
				// can not happen
			}
		}	
	}
	
	/**
	 * Updates all the overlapping positions. Note, all other positions are
	 * automatically updated by their document position updaters.
	 *
	 * @param document the document to has been formatted
	 * @param positions the adapted character positions to be used to update the document positions
	 * @param offset the offset of the document region that has been formatted
	 */
	private void updateAffectedPositions(IDocument document, int[] positions, int offset) {
		
		if (positions.length == 0)
			return;
		
		Map added= new HashMap(positions.length * 2);
		
		for (int i= 0; i < positions.length; i++) {
			
			PositionReference r= (PositionReference) fOverlappingPositionReferences.get(i);
			
			if (r.refersToOffset())
				r.setOffset(offset + positions[i]);
			else
				r.setLength((offset + positions[i]) - r.getOffset());
			
			if (added.get(r.getPosition()) == null) {
				try {
					document.addPosition(r.getCategory(), r.getPosition());
					added.put(r.getPosition(), r.getPosition());
				} catch (BadPositionCategoryException x) {
					// can not happen
				} catch (BadLocationException x) {
					// should not happen
				}
			}	
			
		}
		
		fOverlappingPositionReferences= null;
	} 
	
	/**
	 * Returns the indentation of the line of the given offset.
	 *
	 * @param document the document
	 * @param offset the offset
	 * @return the indentation of the line of the offset
	 */
	private String getIndentation(IDocument document, int offset) {
		
		try {
			int start= document.getLineOfOffset(offset);
			start= document.getLineOffset(start);
			
			int end= start;
			char c= document.getChar(end);
			while ('\t' == c || ' ' == c)
				c= document.getChar(++end);
				
			return document.get(start, end - start);
		} catch (BadLocationException x) {
		}
		
		return ""; //$NON-NLS-1$
	}
	
	/**
	 * Determines whether the offset is the beginning of a line in the given document.
	 *
	 * @param document the document
	 * @param offset the offset
	 * @return <code>true</code> if offset is the beginning of a line
	 * @exception BadLocationException if offset is invalid in document
	 */
	private boolean isLineStart(IDocument document, int offset) throws BadLocationException {
		int start= document.getLineOfOffset(offset);
		start= document.getLineOffset(start);
		return (start == offset);
	}	
}

Back to the top