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