Source code: com/port80/eclipse/editors/util/RegionContentFormatter.java
1 package com.port80.eclipse.editors.util;
2
3 import java.util.ArrayList;
4 import java.util.Collections;
5 import java.util.HashMap;
6 import java.util.List;
7 import java.util.Map;
8
9 import org.eclipse.jface.text.BadLocationException;
10 import org.eclipse.jface.text.BadPositionCategoryException;
11 import org.eclipse.jface.text.DefaultPositionUpdater;
12 import org.eclipse.jface.text.DocumentEvent;
13 import org.eclipse.jface.text.IDocument;
14 import org.eclipse.jface.text.IPositionUpdater;
15 import org.eclipse.jface.text.IRegion;
16 import org.eclipse.jface.text.ITypedRegion;
17 import org.eclipse.jface.text.Position;
18 import org.eclipse.jface.text.TypedPosition;
19 import org.eclipse.jface.text.formatter.IContentFormatter;
20 import org.eclipse.jface.text.formatter.IFormattingStrategy;
21 import org.eclipse.jface.util.Assert;
22
23 /**
24 * Hack of ContentFormatter class to provide information about the region
25 * being formatted so that positions can be updated in IFormattingStrategy.
26 *
27 * @see PerlContentFormatter#format(IDocument, IFormattingStrategy, TypedPosition)
28 * @author chrisl
29 */
30
31 /**
32 * Standard implementation of <code>IContentFormatter</code>.
33 * The formatter supports two operation modi: partition aware and
34 * partition unaware. <p>
35 * In the partition aware mode, the formatter determines the
36 * partitioning of the document region to be formatted. For each
37 * partition it determines all document positions which are affected
38 * when text changes are applied to the partition. Those which overlap
39 * with the partition are remembered as character positions. These
40 * character positions are passed over to the formatting strategy
41 * registered for the partition's content type. The formatting strategy
42 * returns a string containing the formatted document partition as well
43 * as the adapted character positions. The formatted partition replaces
44 * the old content of the partition. The remembered document postions
45 * are updated with the adapted character positions. In addition, all
46 * other document positions are accordingly adapted to the formatting
47 * changes.<p>
48 * In the partition unaware mode, the document's partitioning is ignored
49 * and the document is considered consisting of only one partition of
50 * the content type <code>IDocument.DEFAULT_CONTENT_TYPE</code>. The
51 * formatting process is similar to the partition aware mode, with the
52 * exception of having only one partition.<p>
53 * Usually, clients instantiate this class and configure it before using it.
54 *
55 * @see IContentFormatter
56 * @see IDocument
57 * @see ITypedRegion
58 * @see Position
59 */
60 public class RegionContentFormatter implements IContentFormatter {
61
62 /**
63 * Defines a reference to either the offset or the end offset of
64 * a particular position.
65 */
66 static class PositionReference implements Comparable {
67
68 /** The referenced position */
69 protected Position fPosition;
70 /** The reference to either the offset or the end offset */
71 protected boolean fRefersToOffset;
72 /** The original category of the referenced position */
73 protected String fCategory;
74
75 protected PositionReference(Position position, boolean refersToOffset, String category) {
76 fPosition = position;
77 fRefersToOffset = refersToOffset;
78 fCategory = category;
79 }
80
81 /**
82 * Returns the offset of the referenced position.
83 */
84 protected int getOffset() {
85 return fPosition.getOffset();
86 }
87
88 /**
89 * Manipulates the offset of the referenced position.
90 */
91 protected void setOffset(int offset) {
92 fPosition.setOffset(offset);
93 }
94
95 /**
96 * Returns the length of the referenced position.
97 */
98 protected int getLength() {
99 return fPosition.getLength();
100 }
101
102 /**
103 * Manipulates the length of the referenced position.
104 */
105 protected void setLength(int length) {
106 fPosition.setLength(length);
107 }
108
109 /**
110 * Returns whether this reference points to the offset or endoffset
111 * of the references position.
112 */
113 protected boolean refersToOffset() {
114 return fRefersToOffset;
115 }
116
117 /**
118 * Returns the category of the referenced position.
119 */
120 protected String getCategory() {
121 return fCategory;
122 }
123
124 /**
125 * Returns the referenced position.
126 */
127 protected Position getPosition() {
128 return fPosition;
129 }
130
131 /**
132 * Returns the referenced character position
133 */
134 protected int getCharacterPosition() {
135 if (fRefersToOffset)
136 return getOffset();
137 return getOffset() + getLength();
138 }
139
140 /**
141 * @see Comparable#compareTo(Object)
142 */
143 public int compareTo(Object obj) {
144
145 if (obj instanceof PositionReference) {
146 PositionReference r = (PositionReference) obj;
147 return getCharacterPosition() - r.getCharacterPosition();
148 }
149
150 throw new ClassCastException();
151 }
152 };
153
154 /**
155 * The position updater used to adapt all to update the
156 * remembered partitions.
157 *
158 * @see IPositionUpdater
159 * @see DefaultPositionUpdater
160 */
161 class NonDeletingPositionUpdater extends DefaultPositionUpdater {
162
163 protected NonDeletingPositionUpdater(String category) {
164 super(category);
165 }
166
167 /*
168 * @see DefaultPositionUpdater#notDeleted()
169 */
170 protected boolean notDeleted() {
171 return true;
172 }
173 };
174
175 /**
176 * The position updater which runs as first updater on the document's positions.
177 * Used to remove all affected positions from their categories to avoid them
178 * from being regularily updated.
179 *
180 * @see IPositionUpdater
181 */
182 class RemoveAffectedPositions implements IPositionUpdater {
183 /**
184 * @see IPositionUpdater#update(DocumentEvent)
185 */
186 public void update(DocumentEvent event) {
187 removeAffectedPositions(event.getDocument());
188 }
189 };
190
191 /**
192 * The position updater which runs as last updater on the document's positions.
193 * Used to update all affected positions and adding them back to their
194 * original categories.
195 *
196 * @see IPositionUpdater
197 */
198 class UpdateAffectedPositions implements IPositionUpdater {
199
200 private int[] fPositions;
201 private int fOffset;
202
203 public UpdateAffectedPositions(int[] positions, int offset) {
204 fPositions = positions;
205 fOffset = offset;
206 }
207
208 /**
209 * @see IPositionUpdater#update(DocumentEvent)
210 */
211 public void update(DocumentEvent event) {
212 updateAffectedPositions(event.getDocument(), fPositions, fOffset);
213 }
214 };
215
216 /** Internal position category used for the formatter partitioning */
217 private final static String PARTITIONING = "__formatter_partitioning"; //$NON-NLS-1$
218
219 /** The map of <code>IFormattingStrategy</code> objects */
220 private Map fStrategies;
221 /** The indicator of whether the formatter operates in partition aware mode or not */
222 private boolean fIsPartitionAware = true;
223
224 /** The partition information managing document position categories */
225 private String[] fPartitionManagingCategories;
226 /** The list of references to offset and end offset of all overlapping positions */
227 private List fOverlappingPositionReferences;
228 /** Position updater used for partitioning positions */
229 private IPositionUpdater fPartitioningUpdater;
230
231 /**
232 * Creates a new content formatter. The content formatter operates by default
233 * in the partition-aware mode. There are no preconfigured formatting strategies.
234 */
235 public RegionContentFormatter() {}
236
237 /**
238 * Registers a strategy for a particular content type. If there is already a strategy
239 * registered for this type, the new strategy is registered instead of the old one.
240 * If the given content type is <code>null</code> the given strategy is registered for
241 * all content types as is called only once per formatting session.
242 *
243 * @param strategy the formatting strategy to register, or <code>null</code> to remove an existing one
244 * @param contentType the content type under which to register, or <code>null</code> for all content types
245 */
246 public void setFormattingStrategy(IFormattingStrategy strategy, String contentType) {
247
248 Assert.isNotNull(contentType);
249
250 if (fStrategies == null)
251 fStrategies = new HashMap();
252
253 if (strategy == null)
254 fStrategies.remove(contentType);
255 else
256 fStrategies.put(contentType, strategy);
257 }
258
259 /**
260 * Informs this content formatter about the names of those position categories
261 * which are used to manage the document's partitioning information and thus should
262 * be ignored when this formatter updates positions.
263 *
264 * @param categories the categories to be ignored
265 */
266 public void setPartitionManagingPositionCategories(String[] categories) {
267 fPartitionManagingCategories = categories;
268 }
269
270 /**
271 * Sets the formatter's operation mode.
272 *
273 * @param enable indicates whether the formatting process should be partition ware
274 */
275 public void enablePartitionAwareFormatting(boolean enable) {
276 fIsPartitionAware = enable;
277 }
278
279 /*
280 * @see IContentFormatter#getFormattingStrategy
281 */
282 public IFormattingStrategy getFormattingStrategy(String contentType) {
283
284 Assert.isNotNull(contentType);
285
286 if (fStrategies == null)
287 return null;
288
289 return (IFormattingStrategy) fStrategies.get(contentType);
290 }
291
292 /*
293 * @see IContentFormatter#format
294 */
295 public void format(IDocument document, IRegion region) {
296 if (fIsPartitionAware)
297 formatPartitions(document, region);
298 else
299 formatRegion(document, region);
300 }
301
302 /**
303 * Removes the affected positions from their categories to avoid
304 * that they are invalidly updated.
305 *
306 * @param document the document
307 */
308 void removeAffectedPositions(IDocument document) {
309 int size = fOverlappingPositionReferences.size();
310 for (int i = 0; i < size; i++) {
311 PositionReference r = (PositionReference) fOverlappingPositionReferences.get(i);
312 try {
313 document.removePosition(r.getCategory(), r.getPosition());
314 } catch (BadPositionCategoryException x) {
315 // can not happen
316 }
317 }
318 }
319
320 /**
321 * Updates all the overlapping positions. Note, all other positions are
322 * automatically updated by their document position updaters.
323 *
324 * @param document the document to has been formatted
325 * @param positions the adapted character positions to be used to update the document positions
326 * @param offset the offset of the document region that has been formatted
327 */
328 void updateAffectedPositions(IDocument document, int[] positions, int offset) {
329
330 if (positions.length == 0)
331 return;
332
333 Map added = new HashMap(positions.length * 2);
334
335 for (int i = 0; i < positions.length; i++) {
336
337 PositionReference r = (PositionReference) fOverlappingPositionReferences.get(i);
338
339 if (r.refersToOffset())
340 r.setOffset(offset + positions[i]);
341 else
342 r.setLength((offset + positions[i]) - r.getOffset());
343
344 if (added.get(r.getPosition()) == null) {
345 try {
346 document.addPosition(r.getCategory(), r.getPosition());
347 added.put(r.getPosition(), r.getPosition());
348 } catch (BadPositionCategoryException x) {
349 // can not happen
350 } catch (BadLocationException x) {
351 // should not happen
352 }
353 }
354
355 }
356
357 fOverlappingPositionReferences = null;
358 }
359
360 /**
361 * Determines the partitioning of the given region of the document.
362 * Informs for each partition about the start, the process, and the
363 * termination of the formatting session.
364 */
365 private void formatPartitions(IDocument document, IRegion region) {
366
367 addPartitioningUpdater(document);
368
369 try {
370
371 TypedPosition[] ranges = getPartitioning(document, region);
372 if (ranges != null) {
373 start(ranges, getIndentation(document, region.getOffset()));
374 format(document, ranges);
375 stop(ranges);
376 }
377
378 } catch (BadLocationException x) {}
379
380 removePartitioningUpdater(document);
381 }
382
383 /**
384 * Informs for the given region about the start, the process, and
385 * the termination of the formatting session.
386 */
387 private void formatRegion(IDocument document, IRegion region) {
388
389 IFormattingStrategy strategy = getFormattingStrategy(IDocument.DEFAULT_CONTENT_TYPE);
390 if (strategy != null) {
391 strategy.formatterStarts(getIndentation(document, region.getOffset()));
392 format(
393 document,
394 strategy,
395 new TypedPosition(
396 region.getOffset(),
397 region.getLength(),
398 IDocument.DEFAULT_CONTENT_TYPE));
399 strategy.formatterStops();
400 }
401 }
402
403 /**
404 * Returns the partitioning of the given region of the specified document.
405 * As one partition after the other will be formatted and formatting will
406 * probably change the length of the formatted partition, it must be kept
407 * track of the modifications in order to submit the correct partition to all
408 * formatting strategies. For this, all partitions are remembered as positions
409 * in a dedicated position category. (As formatting stratgies might rely on each
410 * other, calling them in reversed order is not an option.)
411 *
412 * @param document the document
413 * @param region the region for which the partitioning must be determined
414 * @return the partitioning of the specified region
415 * @exception BadLocationException of region is invalid in the document
416 */
417 private TypedPosition[] getPartitioning(IDocument document, IRegion region) throws BadLocationException {
418
419 ITypedRegion[] regions = document.computePartitioning(region.getOffset(), region.getLength());
420 TypedPosition[] positions = new TypedPosition[regions.length];
421
422 for (int i = 0; i < regions.length; i++) {
423 positions[i] = new TypedPosition(regions[i]);
424 try {
425 document.addPosition(PARTITIONING, positions[i]);
426 } catch (BadPositionCategoryException x) {
427 // should not happen
428 }
429 }
430
431 return positions;
432 }
433
434 /**
435 * Fires <code>formatterStarts</code> to all formatter strategies
436 * which will be involved in the forthcoming formatting process.
437 *
438 * @param regions the partitioning of the document to be formatted
439 * @param indentation the initial indentation
440 */
441 private void start(TypedPosition[] regions, String indentation) {
442 for (int i = 0; i < regions.length; i++) {
443 IFormattingStrategy s = getFormattingStrategy(regions[i].getType());
444 if (s != null)
445 s.formatterStarts(indentation);
446 }
447 }
448
449 /**
450 * Formats one partition after the other using the formatter strategy registered for
451 * the partition's content type.
452 *
453 * @param document to document to be formatted
454 * @param ranges the partitioning of the document region to be formatted
455 */
456 private void format(final IDocument document, TypedPosition[] ranges) {
457 for (int i = 0; i < ranges.length; i++) {
458 IFormattingStrategy s = getFormattingStrategy(ranges[i].getType());
459 if (s != null) {
460 format(document, s, ranges[i]);
461 }
462 }
463 }
464
465 /**
466 * Formats the given region of the document using the specified formatting
467 * strategy. In order to maintain positions correctly, first all affected
468 * positions determined, after all document listeners have been informed about
469 * the upcoming change, the affected positions are removed to avoid that they
470 * are regularily updated. After all position updaters have run, the affected
471 * positions are updated with the formatter's information and added back to
472 * their categories, right before the first document listener is informed about
473 * that a change happend.
474 *
475 * @param document the document to be formatted
476 * @param strategy the strategy to be used
477 * @param region the region to be formatted
478 */
479 private void format(final IDocument document, IFormattingStrategy strategy, TypedPosition region) {
480 try {
481
482 final int offset = region.getOffset();
483 int length = region.getLength();
484
485 final int[] positions = getAffectedPositions(document, offset, length);
486 //String content = document.get(offset, length);
487 String formatted =
488 ((IRegionFormattingStrategy)strategy).format(
489 document,
490 region,
491 isLineStart(document, offset),
492 getIndentation(document, offset),
493 positions);
494
495 IPositionUpdater first = new RemoveAffectedPositions();
496 document.insertPositionUpdater(first, 0);
497 IPositionUpdater last = new UpdateAffectedPositions(positions, offset);
498 document.addPositionUpdater(last);
499
500 document.replace(offset, length, formatted);
501
502 document.removePositionUpdater(first);
503 document.removePositionUpdater(last);
504
505 } catch (BadLocationException x) {
506 // should not happen
507 }
508 }
509
510 /**
511 * Fires <code>formatterStops</code> to all formatter strategies which were
512 * involved in the formatting process which is about to terminate.
513 *
514 * @param regions the partitioning of the document which has been formatted
515 */
516 private void stop(TypedPosition[] regions) {
517 for (int i = 0; i < regions.length; i++) {
518 IFormattingStrategy s = getFormattingStrategy(regions[i].getType());
519 if (s != null)
520 s.formatterStops();
521 }
522 }
523
524 /**
525 * Installs those updaters which the formatter needs to keep
526 * track of the partitions.
527 *
528 * @param document the document to be formatted
529 */
530 private void addPartitioningUpdater(IDocument document) {
531 fPartitioningUpdater = new NonDeletingPositionUpdater(PARTITIONING);
532 document.addPositionCategory(PARTITIONING);
533 document.addPositionUpdater(fPartitioningUpdater);
534 }
535
536 /**
537 * Removes the formatter's internal position updater and category.
538 *
539 * @param document the document that has been formatted
540 */
541 private void removePartitioningUpdater(IDocument document) {
542
543 try {
544
545 document.removePositionUpdater(fPartitioningUpdater);
546 document.removePositionCategory(PARTITIONING);
547 fPartitioningUpdater = null;
548
549 } catch (BadPositionCategoryException x) {
550 // should not happen
551 }
552 }
553
554 /**
555 * Determines whether the given document position category should be ignored
556 * by this formatter's position updating.
557 *
558 * @param category the category to check
559 * @return <code>true</code> if the category should be ignored, <code>false</code> otherwise
560 */
561 private boolean ignoreCategory(String category) {
562
563 if (PARTITIONING.equals(category))
564 return true;
565
566 if (fPartitionManagingCategories != null) {
567 for (int i = 0; i < fPartitionManagingCategories.length; i++) {
568 if (fPartitionManagingCategories[i].equals(category))
569 return true;
570 }
571 }
572
573 return false;
574 }
575
576 /**
577 * Determines all embracing, overlapping, and follow up positions
578 * for the given region of the document.
579 *
580 * @param document the document to be formatted
581 * @param offset the offset of the document region to be formatted
582 * @param length the length of the document to be formatted
583 */
584 private void determinePositionsToUpdate(IDocument document, int offset, int length) {
585
586 String[] categories = document.getPositionCategories();
587 if (categories != null) {
588 for (int i = 0; i < categories.length; i++) {
589
590 if (ignoreCategory(categories[i]))
591 continue;
592
593 try {
594
595 Position[] positions = document.getPositions(categories[i]);
596
597 for (int j = 0; j < positions.length; j++) {
598
599 Position p = (Position) positions[j];
600 if (p.overlapsWith(offset, length)) {
601
602 if (offset < p.getOffset())
603 fOverlappingPositionReferences.add(
604 new PositionReference(p, true, categories[i]));
605
606 if (p.getOffset() + p.getLength() < offset + length)
607 fOverlappingPositionReferences.add(
608 new PositionReference(p, false, categories[i]));
609 }
610 }
611
612 } catch (BadPositionCategoryException x) {
613 // can not happen
614 }
615 }
616 }
617 }
618
619 /**
620 * Returns all offset and the end offset of all positions overlapping with the
621 * specified document range.
622 *
623 * @param document the document to be formatted
624 * @param offset the offset of the document region to be formatted
625 * @param length the length of the document to be formatted
626 * @return all character positions of the interleaving positions
627 */
628 private int[] getAffectedPositions(IDocument document, int offset, int length) {
629
630 fOverlappingPositionReferences = new ArrayList();
631
632 determinePositionsToUpdate(document, offset, length);
633
634 Collections.sort(fOverlappingPositionReferences);
635
636 int[] positions = new int[fOverlappingPositionReferences.size()];
637 for (int i = 0; i < positions.length; i++) {
638 PositionReference r = (PositionReference) fOverlappingPositionReferences.get(i);
639 positions[i] = r.getCharacterPosition() - offset;
640 }
641
642 return positions;
643 }
644
645 /**
646 * Returns the indentation of the line of the given offset.
647 *
648 * @param document the document
649 * @param offset the offset
650 * @return the indentation of the line of the offset
651 */
652 private String getIndentation(IDocument document, int offset) {
653
654 try {
655 int start = document.getLineOfOffset(offset);
656 start = document.getLineOffset(start);
657
658 int end = start;
659 char c = document.getChar(end);
660 while ('\t' == c || ' ' == c)
661 c = document.getChar(++end);
662
663 return document.get(start, end - start);
664 } catch (BadLocationException x) {}
665
666 return ""; //$NON-NLS-1$
667 }
668
669 /**
670 * Determines whether the offset is the beginning of a line in the given document.
671 *
672 * @param document the document
673 * @param offset the offset
674 * @return <code>true</code> if offset is the beginning of a line
675 * @exception BadLocationException if offset is invalid in document
676 */
677 private boolean isLineStart(IDocument document, int offset) throws BadLocationException {
678 int start = document.getLineOfOffset(offset);
679 start = document.getLineOffset(start);
680 return (start == offset);
681 }
682 }