1 /*******************************************************************************
2  * Copyright (c) 2000, 2012 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 
15 package org.eclipse.jface.text.presentation;
16 
17 import java.util.HashMap;
18 import java.util.Iterator;
19 import java.util.Map;
20 
21 import org.eclipse.swt.custom.StyleRange;
22 
23 import org.eclipse.core.runtime.Assert;
24 
25 import org.eclipse.jface.text.BadLocationException;
26 import org.eclipse.jface.text.BadPositionCategoryException;
27 import org.eclipse.jface.text.DefaultPositionUpdater;
28 import org.eclipse.jface.text.DocumentEvent;
29 import org.eclipse.jface.text.DocumentPartitioningChangedEvent;
30 import org.eclipse.jface.text.IDocument;
31 import org.eclipse.jface.text.IDocumentExtension3;
32 import org.eclipse.jface.text.IDocumentListener;
33 import org.eclipse.jface.text.IDocumentPartitioningListener;
34 import org.eclipse.jface.text.IDocumentPartitioningListenerExtension;
35 import org.eclipse.jface.text.IDocumentPartitioningListenerExtension2;
36 import org.eclipse.jface.text.IPositionUpdater;
37 import org.eclipse.jface.text.IRegion;
38 import org.eclipse.jface.text.ITextInputListener;
39 import org.eclipse.jface.text.ITextListener;
40 import org.eclipse.jface.text.ITextViewer;
41 import org.eclipse.jface.text.ITextViewerExtension5;
42 import org.eclipse.jface.text.ITypedRegion;
43 import org.eclipse.jface.text.Region;
44 import org.eclipse.jface.text.TextEvent;
45 import org.eclipse.jface.text.TextPresentation;
46 import org.eclipse.jface.text.TextUtilities;
47 import org.eclipse.jface.text.TypedPosition;
48 
49 
50 
51 /**
52  * Standard implementation of <code>IPresentationReconciler</code>. This
53  * implementation assumes that the tasks performed by its presentation damagers
54  * and repairers are lightweight and of low cost. This presentation reconciler
55  * runs in the UI thread and always repairs the complete damage caused by a
56  * document change rather than just the portion overlapping with the viewer's
57  * viewport.
58  * <p>
59  * Usually, clients instantiate this class and configure it before using it.
60  * </p>
61  */
62 public class PresentationReconciler implements IPresentationReconciler, IPresentationReconcilerExtension {
63 
64 	/** Prefix of the name of the position category for tracking damage regions. */
65 	protected final static String TRACKED_PARTITION= "__reconciler_tracked_partition"; //$NON-NLS-1$
66 
67 
68 	/**
69 	 * Internal listener class.
70 	 */
71 	class InternalListener implements
72 			ITextInputListener, IDocumentListener, ITextListener,
73 			IDocumentPartitioningListener, IDocumentPartitioningListenerExtension, IDocumentPartitioningListenerExtension2 {
74 
75 		/** Set to <code>true</code> if between a document about to be changed and a changed event. */
76 		private boolean fDocumentChanging= false;
77 		/**
78 		 * The cached redraw state of the text viewer.
79 		 * @since 3.0
80 		 */
81 		private boolean fCachedRedrawState= true;
82 
83 		@Override
inputDocumentAboutToBeChanged(IDocument oldDocument, IDocument newDocument)84 		public void inputDocumentAboutToBeChanged(IDocument oldDocument, IDocument newDocument) {
85 			if (oldDocument != null) {
86 				try {
87 
88 					fViewer.removeTextListener(this);
89 					oldDocument.removeDocumentListener(this);
90 					oldDocument.removeDocumentPartitioningListener(this);
91 
92 					oldDocument.removePositionUpdater(fPositionUpdater);
93 					oldDocument.removePositionCategory(fPositionCategory);
94 
95 				} catch (BadPositionCategoryException x) {
96 					// should not happened for former input documents;
97 				}
98 			}
99 		}
100 
101 		/*
102 		 * @see ITextInputListener#inputDocumenChanged(IDocument, IDocument)
103 		 */
104 		@Override
inputDocumentChanged(IDocument oldDocument, IDocument newDocument)105 		public void inputDocumentChanged(IDocument oldDocument, IDocument newDocument) {
106 
107 			fDocumentChanging= false;
108 			fCachedRedrawState= true;
109 
110 			if (newDocument != null) {
111 
112 				newDocument.addPositionCategory(fPositionCategory);
113 				newDocument.addPositionUpdater(fPositionUpdater);
114 
115 				newDocument.addDocumentPartitioningListener(this);
116 				newDocument.addDocumentListener(this);
117 				fViewer.addTextListener(this);
118 
119 				setDocumentToDamagers(newDocument);
120 				setDocumentToRepairers(newDocument);
121 				processDamage(new Region(0, newDocument.getLength()), newDocument);
122 			}
123 		}
124 
125 		@Override
documentPartitioningChanged(IDocument document)126 		public void documentPartitioningChanged(IDocument document) {
127 			if (!fDocumentChanging && fCachedRedrawState)
128 				processDamage(new Region(0, document.getLength()), document);
129 			else
130 				fDocumentPartitioningChanged= true;
131 		}
132 
133 		@Override
documentPartitioningChanged(IDocument document, IRegion changedRegion)134 		public void documentPartitioningChanged(IDocument document, IRegion changedRegion) {
135 			if (!fDocumentChanging && fCachedRedrawState) {
136 				processDamage(new Region(changedRegion.getOffset(), changedRegion.getLength()), document);
137 			} else {
138 				fDocumentPartitioningChanged= true;
139 				fChangedDocumentPartitions= changedRegion;
140 			}
141 		}
142 
143 		@Override
documentPartitioningChanged(DocumentPartitioningChangedEvent event)144 		public void documentPartitioningChanged(DocumentPartitioningChangedEvent event) {
145 			IRegion changedRegion= event.getChangedRegion(getDocumentPartitioning());
146 			if (changedRegion != null)
147 				documentPartitioningChanged(event.getDocument(), changedRegion);
148 		}
149 
150 		@Override
documentAboutToBeChanged(DocumentEvent e)151 		public void documentAboutToBeChanged(DocumentEvent e) {
152 
153 			fDocumentChanging= true;
154 			if (fCachedRedrawState) {
155 				try {
156 					int offset= e.getOffset() + e.getLength();
157 					ITypedRegion region= getPartition(e.getDocument(), offset);
158 					fRememberedPosition= new TypedPosition(region);
159 					e.getDocument().addPosition(fPositionCategory, fRememberedPosition);
160 				} catch (BadLocationException x) {
161 					// can not happen
162 				} catch (BadPositionCategoryException x) {
163 					// should not happen on input elements
164 				}
165 			}
166 		}
167 
168 		@Override
documentChanged(DocumentEvent e)169 		public void documentChanged(DocumentEvent e) {
170 			if (fCachedRedrawState) {
171 				try {
172 					e.getDocument().removePosition(fPositionCategory, fRememberedPosition);
173 				} catch (BadPositionCategoryException x) {
174 					// can not happen on input documents
175 				}
176 			}
177 			fDocumentChanging= false;
178 		}
179 
180 		@Override
textChanged(TextEvent e)181 		public void textChanged(TextEvent e) {
182 
183 			fCachedRedrawState= e.getViewerRedrawState();
184 	 		if (!fCachedRedrawState)
185 	 			return;
186 
187 	 		IRegion damage= null;
188 	 		IDocument document= null;
189 
190 		 	if (e.getDocumentEvent() == null) {
191 		 		document= fViewer.getDocument();
192 		 		if (document != null)  {
193 			 		if (e.getOffset() == 0 && e.getLength() == 0 && e.getText() == null) {
194 						// redraw state change, damage the whole document
195 						damage= new Region(0, document.getLength());
196 			 		} else {
197 						IRegion region= widgetRegion2ModelRegion(e);
198 						if (region != null) {
199 							try {
200 								String text= document.get(region.getOffset(), region.getLength());
201 								DocumentEvent de= new DocumentEvent(document, region.getOffset(), region.getLength(), text);
202 								damage= getDamage(de, false);
203 							} catch (BadLocationException x) {
204 							}
205 						}
206 			 		}
207 		 		}
208 		 	} else  {
209 		 		DocumentEvent de= e.getDocumentEvent();
210 		 		document= de.getDocument();
211 		 		damage= getDamage(de, true);
212 		 	}
213 
214 			if (damage != null && document != null)
215 				processDamage(damage, document);
216 
217 			fDocumentPartitioningChanged= false;
218 			fChangedDocumentPartitions= null;
219 		}
220 
221 		/**
222 		 * Translates the given text event into the corresponding range of the viewer's document.
223 		 *
224 		 * @param e the text event
225 		 * @return the widget region corresponding the region of the given event or
226 		 *         <code>null</code> if none
227 		 * @since 2.1
228 		 */
widgetRegion2ModelRegion(TextEvent e)229 		protected IRegion widgetRegion2ModelRegion(TextEvent e) {
230 
231 			String text= e.getText();
232 			int length= text == null ? 0 : text.length();
233 
234 			if (fViewer instanceof ITextViewerExtension5) {
235 				ITextViewerExtension5 extension= (ITextViewerExtension5) fViewer;
236 				return extension.widgetRange2ModelRange(new Region(e.getOffset(), length));
237 			}
238 
239 			IRegion visible= fViewer.getVisibleRegion();
240 			IRegion region= new Region(e.getOffset() + visible.getOffset(), length);
241 			return region;
242 		}
243 	}
244 
245 	/** The map of presentation damagers. */
246 	private Map<String, IPresentationDamager> fDamagers;
247 	/** The map of presentation repairers. */
248 	private Map<String, IPresentationRepairer> fRepairers;
249 	/** The target viewer. */
250 	private ITextViewer fViewer;
251 	/** The internal listener. */
252 	private InternalListener fInternalListener= new InternalListener();
253 	/** The name of the position category to track damage regions. */
254 	private String fPositionCategory;
255 	/** The position updated for the damage regions' position category. */
256 	private IPositionUpdater fPositionUpdater;
257 	/** The positions representing the damage regions. */
258 	private TypedPosition fRememberedPosition;
259 	/** Flag indicating the receipt of a partitioning changed notification. */
260 	private boolean fDocumentPartitioningChanged= false;
261 	/** The range covering the changed partitioning. */
262 	private IRegion fChangedDocumentPartitions= null;
263 	/**
264 	 * The partitioning used by this presentation reconciler.
265 	 * @since 3.0
266 	 */
267 	private String fPartitioning;
268 
269 	/**
270 	 * Creates a new presentation reconciler. There are no damagers or repairers
271 	 * registered with this reconciler by default. The default partitioning
272 	 * <code>IDocumentExtension3.DEFAULT_PARTITIONING</code> is used.
273 	 */
PresentationReconciler()274 	public PresentationReconciler() {
275 		super();
276 		fPartitioning= IDocumentExtension3.DEFAULT_PARTITIONING;
277 		fPositionCategory= TRACKED_PARTITION + hashCode();
278 		fPositionUpdater= new DefaultPositionUpdater(fPositionCategory);
279 	}
280 
281 	/**
282 	 * Sets the document partitioning for this presentation reconciler.
283 	 *
284 	 * @param partitioning the document partitioning for this presentation reconciler.
285 	 * @since 3.0
286 	 */
setDocumentPartitioning(String partitioning)287 	public void setDocumentPartitioning(String partitioning) {
288 		Assert.isNotNull(partitioning);
289 		fPartitioning= partitioning;
290 	}
291 
292 	/*
293 	 * @see org.eclipse.jface.text.presentation.IPresentationReconcilerExtension#geDocumenttPartitioning()
294 	 * @since 3.0
295 	 */
296 	@Override
getDocumentPartitioning()297 	public String getDocumentPartitioning() {
298 		return fPartitioning;
299 	}
300 
301 	/**
302 	 * Registers the given presentation damager for a particular content type.
303 	 * If there is already a damager registered for this type, the old damager
304 	 * is removed first.
305 	 *
306 	 * @param damager the presentation damager to register, or <code>null</code> to remove an existing one
307 	 * @param contentType the content type under which to register
308 	 */
setDamager(IPresentationDamager damager, String contentType)309 	public void setDamager(IPresentationDamager damager, String contentType) {
310 
311 		Assert.isNotNull(contentType);
312 
313 		if (fDamagers == null)
314 			fDamagers= new HashMap<>();
315 
316 		if (damager == null)
317 			fDamagers.remove(contentType);
318 		else
319 			fDamagers.put(contentType, damager);
320 	}
321 
322 	/**
323 	 * Registers the given presentation repairer for a particular content type.
324 	 * If there is already a repairer registered for this type, the old repairer
325 	 * is removed first.
326 	 *
327 	 * @param repairer the presentation repairer to register, or <code>null</code> to remove an existing one
328 	 * @param contentType the content type under which to register
329 	 */
setRepairer(IPresentationRepairer repairer, String contentType)330 	public void setRepairer(IPresentationRepairer repairer, String contentType) {
331 
332 		Assert.isNotNull(contentType);
333 
334 		if (fRepairers == null)
335 			fRepairers= new HashMap<>();
336 
337 		if (repairer == null)
338 			fRepairers.remove(contentType);
339 		else
340 			fRepairers.put(contentType, repairer);
341 	}
342 
343 	@Override
install(ITextViewer viewer)344 	public void install(ITextViewer viewer) {
345 		Assert.isNotNull(viewer);
346 
347 		fViewer= viewer;
348 		fViewer.addTextInputListener(fInternalListener);
349 
350 		IDocument document= viewer.getDocument();
351 		if (document != null)
352 			fInternalListener.inputDocumentChanged(null, document);
353 	}
354 
355 	@Override
uninstall()356 	public void uninstall() {
357 		fViewer.removeTextInputListener(fInternalListener);
358 
359 		// Ensure we uninstall all listeners
360 		fInternalListener.inputDocumentAboutToBeChanged(fViewer.getDocument(), null);
361 	}
362 
363 	@Override
getDamager(String contentType)364 	public IPresentationDamager getDamager(String contentType) {
365 
366 		if (fDamagers == null)
367 			return null;
368 
369 		return fDamagers.get(contentType);
370 	}
371 
372 	@Override
getRepairer(String contentType)373 	public IPresentationRepairer getRepairer(String contentType) {
374 
375 		if (fRepairers == null)
376 			return null;
377 
378 		return fRepairers.get(contentType);
379 	}
380 
381 	/**
382 	 * Informs all registered damagers about the document on which they will work.
383 	 *
384 	 * @param document the document on which to work
385 	 */
setDocumentToDamagers(IDocument document)386 	protected void setDocumentToDamagers(IDocument document) {
387 		if (fDamagers != null) {
388 			Iterator<IPresentationDamager> e= fDamagers.values().iterator();
389 			while (e.hasNext()) {
390 				IPresentationDamager damager= e.next();
391 				damager.setDocument(document);
392 			}
393 		}
394 	}
395 
396 	/**
397 	 * Informs all registered repairers about the document on which they will work.
398 	 *
399 	 * @param document the document on which to work
400 	 */
setDocumentToRepairers(IDocument document)401 	protected void setDocumentToRepairers(IDocument document) {
402 		if (fRepairers != null) {
403 			Iterator<IPresentationRepairer> e= fRepairers.values().iterator();
404 			while (e.hasNext()) {
405 				IPresentationRepairer repairer= e.next();
406 				repairer.setDocument(document);
407 			}
408 		}
409 	}
410 
411 	/**
412 	 * Constructs a "repair description" for the given damage and returns this
413 	 * description as a text presentation. For this, it queries the partitioning
414 	 * of the damage region and asks the appropriate presentation repairer for
415 	 * each partition to construct the "repair description" for this partition.
416 	 *
417 	 * @param damage the damage to be repaired
418 	 * @param document the document whose presentation must be repaired
419 	 * @return the presentation repair description as text presentation or
420 	 *         <code>null</code> if the partitioning could not be computed
421 	 */
createPresentation(IRegion damage, IDocument document)422 	protected TextPresentation createPresentation(IRegion damage, IDocument document) {
423 		try {
424 			if (fRepairers == null || fRepairers.isEmpty()) {
425 				TextPresentation presentation= new TextPresentation(damage, 100);
426 				presentation.setDefaultStyleRange(new StyleRange(damage.getOffset(), damage.getLength(), null, null));
427 				return presentation;
428 			}
429 
430 			TextPresentation presentation= new TextPresentation(damage, 1000);
431 
432 			ITypedRegion[] partitioning= TextUtilities.computePartitioning(document, getDocumentPartitioning(), damage.getOffset(), damage.getLength(), false);
433 			for (ITypedRegion r : partitioning) {
434 				IPresentationRepairer repairer= getRepairer(r.getType());
435 				if (repairer != null)
436 					repairer.createPresentation(presentation, r);
437 			}
438 
439 			return presentation;
440 
441 		} catch (BadLocationException x) {
442 			return null;
443 		}
444 	}
445 
446 
447 	/**
448 	 * Checks for the first and the last affected partition affected by a
449 	 * document event and calls their damagers. Invalidates everything from the
450 	 * start of the damage for the first partition until the end of the damage
451 	 * for the last partition.
452 	 *
453 	 * @param e the event describing the document change
454 	 * @param optimize <code>true</code> if partition changes should be
455 	 *        considered for optimization
456 	 * @return the damaged caused by the change or <code>null</code> if
457 	 *         computing the partitioning failed
458 	 * @since 3.0
459 	 */
getDamage(DocumentEvent e, boolean optimize)460 	private IRegion getDamage(DocumentEvent e, boolean optimize) {
461 		int length= e.getText() == null ? 0 : e.getText().length();
462 
463 		if (fDamagers == null || fDamagers.isEmpty()) {
464 			length= Math.max(e.getLength(), length);
465 			length= Math.min(e.getDocument().getLength() - e.getOffset(), length);
466 			return new Region(e.getOffset(), length);
467 		}
468 
469 		boolean isDeletion= length == 0;
470 		IRegion damage= null;
471 		try {
472 			int offset= e.getOffset();
473 			if (isDeletion)
474 				offset= Math.max(0, offset - 1);
475 			ITypedRegion partition= getPartition(e.getDocument(), offset);
476 			IPresentationDamager damager= getDamager(partition.getType());
477 			if (damager == null)
478 				return null;
479 
480 			IRegion r= damager.getDamageRegion(partition, e, fDocumentPartitioningChanged);
481 
482 			if (!fDocumentPartitioningChanged && optimize && !isDeletion) {
483 				damage= r;
484 			} else {
485 
486 				int damageStart= r.getOffset();
487 				int damageEnd= getDamageEndOffset(e);
488 
489 				if (fChangedDocumentPartitions != null) {
490 					damageStart= Math.min(damageStart, fChangedDocumentPartitions.getOffset());
491 					damageEnd= Math.max(damageEnd, fChangedDocumentPartitions.getOffset() + fChangedDocumentPartitions.getLength());
492 				}
493 
494 				damage= damageEnd == -1 ? r : new Region(damageStart, damageEnd - damageStart);
495 			}
496 
497 		} catch (BadLocationException x) {
498 		}
499 
500 		return damage;
501 	}
502 
503 	/**
504 	 * Returns the end offset of the damage. If a partition has been split by
505 	 * the given document event also the second half of the original
506 	 * partition must be considered. This is achieved by using the remembered
507 	 * partition range.
508 	 *
509 	 * @param e the event describing the change
510 	 * @return the damage end offset (excluding)
511 	 * @exception BadLocationException if method accesses invalid offset
512 	 */
getDamageEndOffset(DocumentEvent e)513 	private int getDamageEndOffset(DocumentEvent e) throws BadLocationException {
514 
515 		IDocument d= e.getDocument();
516 
517 		int length= 0;
518 		if (e.getText() != null) {
519 			length= e.getText().length();
520 			if (length > 0)
521 				-- length;
522 		}
523 
524 		ITypedRegion partition= getPartition(d, e.getOffset() + length);
525 		int endOffset= partition.getOffset() + partition.getLength();
526 		if (endOffset == e.getOffset())
527 			return -1;
528 
529 		int end= fRememberedPosition == null ? -1 : fRememberedPosition.getOffset() + fRememberedPosition.getLength();
530 		if (endOffset < end && end < d.getLength())
531 			partition= getPartition(d, end);
532 
533 		IPresentationDamager damager= getDamager(partition.getType());
534 		if (damager == null)
535 			return -1;
536 
537 		IRegion r= damager.getDamageRegion(partition, e, fDocumentPartitioningChanged);
538 
539 		return r.getOffset() + r.getLength();
540 	}
541 
542 	/**
543 	 * Processes the given damage.
544 	 * @param damage the damage to be repaired
545 	 * @param document the document whose presentation must be repaired
546 	 */
processDamage(IRegion damage, IDocument document)547 	private void processDamage(IRegion damage, IDocument document) {
548 		if (damage != null && damage.getLength() > 0) {
549 			TextPresentation p= createPresentation(damage, document);
550 			if (p != null)
551 				applyTextRegionCollection(p);
552 		}
553 	}
554 
555 	/**
556 	 * Applies the given text presentation to the text viewer the presentation
557 	 * reconciler is installed on.
558 	 *
559 	 * @param presentation the text presentation to be applied to the text viewer
560 	 */
applyTextRegionCollection(TextPresentation presentation)561 	private void applyTextRegionCollection(TextPresentation presentation) {
562 		fViewer.changeTextPresentation(presentation, false);
563 	}
564 
565 	/**
566 	 * Returns the partition for the given offset in the given document.
567 	 *
568 	 * @param document the document
569 	 * @param offset the offset
570 	 * @return the partition
571 	 * @throws BadLocationException if offset is invalid in the given document
572 	 * @since 3.0
573 	 */
getPartition(IDocument document, int offset)574 	private ITypedRegion getPartition(IDocument document, int offset) throws BadLocationException {
575 		return TextUtilities.getPartition(document, getDocumentPartitioning(), offset, false);
576 	}
577 }
578