001: /*******************************************************************************
002: * Copyright (c) 2000, 2007 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.jface.text.reconciler;
011:
012: import org.eclipse.core.runtime.Assert;
013: import org.eclipse.core.runtime.IProgressMonitor;
014:
015: import org.eclipse.jface.text.DocumentEvent;
016: import org.eclipse.jface.text.IDocument;
017: import org.eclipse.jface.text.IDocumentListener;
018: import org.eclipse.jface.text.ITextInputListener;
019: import org.eclipse.jface.text.ITextViewer;
020:
021: /**
022: * Abstract implementation of {@link IReconciler}. The reconciler
023: * listens to input document changes as well as changes of
024: * the input document of the text viewer it is installed on. Depending on
025: * its configuration it manages the received change notifications in a
026: * queue folding neighboring or overlapping changes together. The reconciler
027: * processes the dirty regions as a background activity after having waited for further
028: * changes for the configured duration of time. A reconciler is started using the
029: * {@link #install(ITextViewer)} method. As a first step {@link #initialProcess()} is
030: * executed in the background. Then, the reconciling thread waits for changes that
031: * need to be reconciled. A reconciler can be resumed by calling {@link #forceReconciling()}
032: * independent from the existence of actual changes. This mechanism is for subclasses only.
033: * It is the clients responsibility to stop a reconciler using its {@link #uninstall()}
034: * method. Unstopped reconcilers do not free their resources.
035: * <p>
036: * It is subclass responsibility to specify how dirty regions are processed.
037: * </p>
038: *
039: * @see org.eclipse.jface.text.IDocumentListener
040: * @see org.eclipse.jface.text.ITextInputListener
041: * @see org.eclipse.jface.text.reconciler.DirtyRegion
042: * @since 2.0
043: */
044: abstract public class AbstractReconciler implements IReconciler {
045:
046: /**
047: * Background thread for the reconciling activity.
048: */
049: class BackgroundThread extends Thread {
050:
051: /** Has the reconciler been canceled. */
052: private boolean fCanceled = false;
053: /** Has the reconciler been reset. */
054: private boolean fReset = false;
055: /** Some changes need to be processed. */
056: private boolean fIsDirty = false;
057: /** Is a reconciling strategy active. */
058: private boolean fIsActive = false;
059:
060: /**
061: * Creates a new background thread. The thread
062: * runs with minimal priority.
063: *
064: * @param name the thread's name
065: */
066: public BackgroundThread(String name) {
067: super (name);
068: setPriority(Thread.MIN_PRIORITY);
069: setDaemon(true);
070: }
071:
072: /**
073: * Returns whether a reconciling strategy is active right now.
074: *
075: * @return <code>true</code> if a activity is active
076: */
077: public boolean isActive() {
078: return fIsActive;
079: }
080:
081: /**
082: * Returns whether some changes need to be processed.
083: *
084: * @return <code>true</code> if changes wait to be processed
085: * @since 3.0
086: */
087: public synchronized boolean isDirty() {
088: return fIsDirty;
089: }
090:
091: /**
092: * Cancels the background thread.
093: */
094: public void cancel() {
095: fCanceled = true;
096: IProgressMonitor pm = fProgressMonitor;
097: if (pm != null)
098: pm.setCanceled(true);
099: synchronized (fDirtyRegionQueue) {
100: fDirtyRegionQueue.notifyAll();
101: }
102: }
103:
104: /**
105: * Suspends the caller of this method until this background thread has
106: * emptied the dirty region queue.
107: */
108: public void suspendCallerWhileDirty() {
109: boolean isDirty;
110: do {
111: synchronized (fDirtyRegionQueue) {
112: isDirty = fDirtyRegionQueue.getSize() > 0;
113: if (isDirty) {
114: try {
115: fDirtyRegionQueue.wait();
116: } catch (InterruptedException x) {
117: }
118: }
119: }
120: } while (isDirty);
121: }
122:
123: /**
124: * Reset the background thread as the text viewer has been changed,
125: */
126: public void reset() {
127:
128: if (fDelay > 0) {
129:
130: synchronized (this ) {
131: fIsDirty = true;
132: fReset = true;
133: }
134:
135: } else {
136:
137: synchronized (this ) {
138: fIsDirty = true;
139: }
140:
141: synchronized (fDirtyRegionQueue) {
142: fDirtyRegionQueue.notifyAll();
143: }
144: }
145:
146: reconcilerReset();
147: }
148:
149: /**
150: * The background activity. Waits until there is something in the
151: * queue managing the changes that have been applied to the text viewer.
152: * Removes the first change from the queue and process it.
153: * <p>
154: * Calls {@link AbstractReconciler#initialProcess()} on entrance.
155: * </p>
156: */
157: public void run() {
158:
159: synchronized (fDirtyRegionQueue) {
160: try {
161: fDirtyRegionQueue.wait(fDelay);
162: } catch (InterruptedException x) {
163: }
164: }
165:
166: initialProcess();
167:
168: while (!fCanceled) {
169:
170: synchronized (fDirtyRegionQueue) {
171: try {
172: fDirtyRegionQueue.wait(fDelay);
173: } catch (InterruptedException x) {
174: }
175: }
176:
177: if (fCanceled)
178: break;
179:
180: if (!isDirty())
181: continue;
182:
183: synchronized (this ) {
184: if (fReset) {
185: fReset = false;
186: continue;
187: }
188: }
189:
190: DirtyRegion r = null;
191: synchronized (fDirtyRegionQueue) {
192: r = fDirtyRegionQueue.removeNextDirtyRegion();
193: }
194:
195: fIsActive = true;
196:
197: if (fProgressMonitor != null)
198: fProgressMonitor.setCanceled(false);
199:
200: process(r);
201:
202: synchronized (fDirtyRegionQueue) {
203: if (0 == fDirtyRegionQueue.getSize()) {
204: synchronized (this ) {
205: fIsDirty = fProgressMonitor != null ? fProgressMonitor
206: .isCanceled()
207: : false;
208: }
209: fDirtyRegionQueue.notifyAll();
210: }
211: }
212:
213: fIsActive = false;
214: }
215: }
216: }
217:
218: /**
219: * Internal document listener and text input listener.
220: */
221: class Listener implements IDocumentListener, ITextInputListener {
222:
223: /*
224: * @see IDocumentListener#documentAboutToBeChanged(DocumentEvent)
225: */
226: public void documentAboutToBeChanged(DocumentEvent e) {
227: }
228:
229: /*
230: * @see IDocumentListener#documentChanged(DocumentEvent)
231: */
232: public void documentChanged(DocumentEvent e) {
233:
234: if (!fThread.isDirty() && fThread.isAlive()) {
235: if (!fIsAllowedToModifyDocument
236: && Thread.currentThread() == fThread)
237: throw new UnsupportedOperationException(
238: "The reconciler thread is not allowed to modify the document"); //$NON-NLS-1$
239: aboutToBeReconciled();
240: }
241:
242: /*
243: * The second OR condition handles the case when the document
244: * gets changed while still inside initialProcess().
245: */
246: if (fProgressMonitor != null
247: && (fThread.isActive() || fThread.isDirty()
248: && fThread.isAlive()))
249: fProgressMonitor.setCanceled(true);
250:
251: if (fIsIncrementalReconciler)
252: createDirtyRegion(e);
253:
254: fThread.reset();
255:
256: }
257:
258: /*
259: * @see ITextInputListener#inputDocumentAboutToBeChanged(IDocument, IDocument)
260: */
261: public void inputDocumentAboutToBeChanged(IDocument oldInput,
262: IDocument newInput) {
263:
264: if (oldInput == fDocument) {
265:
266: if (fDocument != null)
267: fDocument.removeDocumentListener(this );
268:
269: if (fIsIncrementalReconciler) {
270: synchronized (fDirtyRegionQueue) {
271: fDirtyRegionQueue.purgeQueue();
272: }
273: if (fDocument != null && fDocument.getLength() > 0) {
274: DocumentEvent e = new DocumentEvent(fDocument,
275: 0, fDocument.getLength(), ""); //$NON-NLS-1$
276: createDirtyRegion(e);
277: fThread.reset();
278: fThread.suspendCallerWhileDirty();
279: }
280: }
281:
282: fDocument = null;
283: }
284: }
285:
286: /*
287: * @see ITextInputListener#inputDocumentChanged(IDocument, IDocument)
288: */
289: public void inputDocumentChanged(IDocument oldInput,
290: IDocument newInput) {
291:
292: fDocument = newInput;
293: if (fDocument == null)
294: return;
295:
296: reconcilerDocumentChanged(fDocument);
297:
298: fDocument.addDocumentListener(this );
299:
300: if (!fThread.isDirty())
301: aboutToBeReconciled();
302:
303: if (fIsIncrementalReconciler) {
304: DocumentEvent e = new DocumentEvent(fDocument, 0, 0,
305: fDocument.get());
306: createDirtyRegion(e);
307: }
308:
309: startReconciling();
310: }
311: }
312:
313: /** Queue to manage the changes applied to the text viewer. */
314: private DirtyRegionQueue fDirtyRegionQueue;
315: /** The background thread. */
316: private BackgroundThread fThread;
317: /** Internal document and text input listener. */
318: private Listener fListener;
319: /** The background thread delay. */
320: private int fDelay = 500;
321: /** Are there incremental reconciling strategies? */
322: private boolean fIsIncrementalReconciler = true;
323: /** The progress monitor used by this reconciler. */
324: private IProgressMonitor fProgressMonitor;
325: /**
326: * Tells whether this reconciler is allowed to modify the document.
327: * @since 3.2
328: */
329: private boolean fIsAllowedToModifyDocument = true;
330:
331: /** The text viewer's document. */
332: private IDocument fDocument;
333: /** The text viewer */
334: private ITextViewer fViewer;
335:
336: /**
337: * Processes a dirty region. If the dirty region is <code>null</code> the whole
338: * document is consider being dirty. The dirty region is partitioned by the
339: * document and each partition is handed over to a reconciling strategy registered
340: * for the partition's content type.
341: *
342: * @param dirtyRegion the dirty region to be processed
343: */
344: abstract protected void process(DirtyRegion dirtyRegion);
345:
346: /**
347: * Hook called when the document whose contents should be reconciled
348: * has been changed, i.e., the input document of the text viewer this
349: * reconciler is installed on. Usually, subclasses use this hook to
350: * inform all their reconciling strategies about the change.
351: *
352: * @param newDocument the new reconciler document
353: */
354: abstract protected void reconcilerDocumentChanged(
355: IDocument newDocument);
356:
357: /**
358: * Creates a new reconciler without configuring it.
359: */
360: protected AbstractReconciler() {
361: super ();
362: }
363:
364: /**
365: * Tells the reconciler how long it should wait for further text changes before
366: * activating the appropriate reconciling strategies.
367: *
368: * @param delay the duration in milliseconds of a change collection period.
369: */
370: public void setDelay(int delay) {
371: fDelay = delay;
372: }
373:
374: /**
375: * Tells the reconciler whether any of the available reconciling strategies
376: * is interested in getting detailed dirty region information or just in the
377: * fact that the document has been changed. In the second case, the reconciling
378: * can not incrementally be pursued.
379: *
380: * @param isIncremental indicates whether this reconciler will be configured with
381: * incremental reconciling strategies
382: *
383: * @see DirtyRegion
384: * @see IReconcilingStrategy
385: */
386: public void setIsIncrementalReconciler(boolean isIncremental) {
387: fIsIncrementalReconciler = isIncremental;
388: }
389:
390: /**
391: * Tells the reconciler whether it is allowed to change the document
392: * inside its reconciler thread.
393: * <p>
394: * If this is set to <code>false</code> an {@link UnsupportedOperationException}
395: * will be thrown when this restriction will be violated.
396: * </p>
397: *
398: * @param isAllowedToModify indicates whether this reconciler is allowed to modify the document
399: * @since 3.2
400: */
401: public void setIsAllowedToModifyDocument(boolean isAllowedToModify) {
402: fIsAllowedToModifyDocument = isAllowedToModify;
403: }
404:
405: /**
406: * Sets the progress monitor of this reconciler.
407: *
408: * @param monitor the monitor to be used
409: */
410: public void setProgressMonitor(IProgressMonitor monitor) {
411: fProgressMonitor = monitor;
412: }
413:
414: /**
415: * Returns whether any of the reconciling strategies is interested in
416: * detailed dirty region information.
417: *
418: * @return whether this reconciler is incremental
419: *
420: * @see IReconcilingStrategy
421: */
422: protected boolean isIncrementalReconciler() {
423: return fIsIncrementalReconciler;
424: }
425:
426: /**
427: * Returns the input document of the text viewer this reconciler is installed on.
428: *
429: * @return the reconciler document
430: */
431: protected IDocument getDocument() {
432: return fDocument;
433: }
434:
435: /**
436: * Returns the text viewer this reconciler is installed on.
437: *
438: * @return the text viewer this reconciler is installed on
439: */
440: protected ITextViewer getTextViewer() {
441: return fViewer;
442: }
443:
444: /**
445: * Returns the progress monitor of this reconciler.
446: *
447: * @return the progress monitor of this reconciler
448: */
449: protected IProgressMonitor getProgressMonitor() {
450: return fProgressMonitor;
451: }
452:
453: /*
454: * @see IReconciler#install(ITextViewer)
455: */
456: public void install(ITextViewer textViewer) {
457:
458: Assert.isNotNull(textViewer);
459: fViewer = textViewer;
460:
461: synchronized (this ) {
462: if (fThread != null)
463: return;
464: fThread = new BackgroundThread(getClass().getName());
465: }
466:
467: fDirtyRegionQueue = new DirtyRegionQueue();
468:
469: fListener = new Listener();
470: fViewer.addTextInputListener(fListener);
471:
472: // see bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=67046
473: // if the reconciler gets installed on a viewer that already has a document
474: // (e.g. when reusing editors), we force the listener to register
475: // itself as document listener, because there will be no input change
476: // on the viewer.
477: // In order to do that, we simulate an input change.
478: IDocument document = textViewer.getDocument();
479: if (document != null) {
480: fListener
481: .inputDocumentAboutToBeChanged(fDocument, document);
482: fListener.inputDocumentChanged(fDocument, document);
483: }
484: }
485:
486: /*
487: * @see IReconciler#uninstall()
488: */
489: public void uninstall() {
490: if (fListener != null) {
491:
492: fViewer.removeTextInputListener(fListener);
493: if (fDocument != null) {
494: fListener
495: .inputDocumentAboutToBeChanged(fDocument, null);
496: fListener.inputDocumentChanged(fDocument, null);
497: }
498: fListener = null;
499:
500: synchronized (this ) {
501: // http://dev.eclipse.org/bugs/show_bug.cgi?id=19135
502: BackgroundThread bt = fThread;
503: fThread = null;
504: bt.cancel();
505: }
506: }
507: }
508:
509: /**
510: * Creates a dirty region for a document event and adds it to the queue.
511: *
512: * @param e the document event for which to create a dirty region
513: */
514: private void createDirtyRegion(DocumentEvent e) {
515: synchronized (fDirtyRegionQueue) {
516: if (e.getLength() == 0 && e.getText() != null) {
517: // Insert
518: fDirtyRegionQueue.addDirtyRegion(new DirtyRegion(e
519: .getOffset(), e.getText().length(),
520: DirtyRegion.INSERT, e.getText()));
521:
522: } else if (e.getText() == null || e.getText().length() == 0) {
523: // Remove
524: fDirtyRegionQueue.addDirtyRegion(new DirtyRegion(e
525: .getOffset(), e.getLength(),
526: DirtyRegion.REMOVE, null));
527:
528: } else {
529: // Replace (Remove + Insert)
530: fDirtyRegionQueue.addDirtyRegion(new DirtyRegion(e
531: .getOffset(), e.getLength(),
532: DirtyRegion.REMOVE, null));
533: fDirtyRegionQueue.addDirtyRegion(new DirtyRegion(e
534: .getOffset(), e.getText().length(),
535: DirtyRegion.INSERT, e.getText()));
536: }
537: }
538: }
539:
540: /**
541: * Hook for subclasses which want to perform some
542: * action as soon as reconciliation is needed.
543: * <p>
544: * Default implementation is to do nothing.
545: * </p>
546: *
547: * @since 3.0
548: */
549: protected void aboutToBeReconciled() {
550: }
551:
552: /**
553: * This method is called on startup of the background activity. It is called only
554: * once during the life time of the reconciler. Clients may reimplement this method.
555: */
556: protected void initialProcess() {
557: }
558:
559: /**
560: * Forces the reconciler to reconcile the structure of the whole document.
561: * Clients may extend this method.
562: */
563: protected void forceReconciling() {
564:
565: if (fDocument != null) {
566:
567: if (!fThread.isDirty() && fThread.isAlive())
568: aboutToBeReconciled();
569:
570: if (fProgressMonitor != null && fThread.isActive())
571: fProgressMonitor.setCanceled(true);
572:
573: if (fIsIncrementalReconciler) {
574: DocumentEvent e = new DocumentEvent(fDocument, 0,
575: fDocument.getLength(), fDocument.get());
576: createDirtyRegion(e);
577: }
578:
579: startReconciling();
580: }
581: }
582:
583: /**
584: * Starts the reconciler to reconcile the queued dirty-regions.
585: * Clients may extend this method.
586: */
587: protected synchronized void startReconciling() {
588: if (fThread == null)
589: return;
590:
591: if (!fThread.isAlive()) {
592: try {
593: fThread.start();
594: } catch (IllegalThreadStateException e) {
595: // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=40549
596: // This is the only instance where the thread is started; since
597: // we checked that it is not alive, it must be dead already due
598: // to a run-time exception or error. Exit.
599: }
600: } else {
601: fThread.reset();
602: }
603: }
604:
605: /**
606: * Hook that is called after the reconciler thread has been reset.
607: */
608: protected void reconcilerReset() {
609: }
610: }
|