blob: be8bf83cbb30c9084947081628b693db4b5d180c [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2003 IBM Corporation 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 API and implementation
*******************************************************************************/
package org.eclipse.jface.text.reconciler;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.Assert;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.ITextInputListener;
import org.eclipse.jface.text.ITextViewer;
/**
* Abstract implementation of <code>IReconciler</code>. The reconciler
* listens to input document changes as well as changes of
* the input document of the text viewer it is installed on. Depending on
* its configuration it manages the received change notifications in a
* queue folding neighboring or overlapping changes together. The reconciler
* processes the dirty regions as a background activity after having waited for further
* changes for the configured duration of time. A reconciler is started using its
* <code>install</code> method. As a first step <code>initialProcess</code> is
* executed in the background. Then, the reconciling thread waits for changes that
* need to be reconciled. A reconciler can be resumed by calling <code>forceReconciling</code>
* independent from the existence of actual changes. This mechanism is for subclasses only.
* It is the clients responsibility to stop a reconciler using its <code>uninstall</code>
* method. Unstopped reconcilers do not free their resources.<p>
* It is subclass responsibility to specify how dirty regions are processed.
*
* @see IReconciler
* @see IDocumentListener
* @see ITextInputListener
* @see DirtyRegion
* @since 2.0
*/
abstract public class AbstractReconciler implements IReconciler {
/**
* Background thread for the reconciling activity.
*/
class BackgroundThread extends Thread {
/** Has the reconciler been canceled */
private boolean fCanceled= false;
/** Has the reconciler been reset */
private boolean fReset= false;
/** Has a change been applied */
private boolean fIsDirty= false;
/** Is a reconciling strategy active */
private boolean fIsActive= false;
/**
* Creates a new background thread. The thread
* runs with minimal priority.
*
* @param name the thread's name
*/
public BackgroundThread(String name) {
super(name);
setPriority(Thread.MIN_PRIORITY);
setDaemon(true);
}
/**
* Returns whether a reconciling strategy is active right now.
*
* @return <code>true</code> if a activity is active
*/
public boolean isActive() {
return fIsActive;
}
/**
* Cancels the background thread.
*/
public void cancel() {
fCanceled= true;
synchronized (fDirtyRegionQueue) {
fDirtyRegionQueue.notifyAll();
}
}
/**
* Suspends the caller of this method until this background thread has
* emptied the dirty region queue.
*/
public void suspendCallerWhileDirty() {
while (fIsDirty) {
synchronized (fDirtyRegionQueue) {
if (fIsDirty) {
try {
fDirtyRegionQueue.wait();
} catch (InterruptedException x) {
}
}
}
}
}
/**
* Reset the background thread as the text viewer has been changed,
*/
public void reset() {
if (fDelay > 0) {
synchronized (this) {
fIsDirty= true;
fReset= true;
}
} else {
synchronized(this) {
fIsDirty= true;
}
synchronized (fDirtyRegionQueue) {
fDirtyRegionQueue.notifyAll();
}
}
reconcilerReset();
}
/**
* The background activity. Waits until there is something in the
* queue managing the changes that have been applied to the text viewer.
* Removes the first change from the queue and process it.<p>
* Calls <code>initialProcess</code> on entrance.
*/
public void run() {
synchronized (fDirtyRegionQueue) {
try {
fDirtyRegionQueue.wait(fDelay);
} catch (InterruptedException x) {
}
}
initialProcess();
while (!fCanceled) {
synchronized (fDirtyRegionQueue) {
try {
fDirtyRegionQueue.wait(fDelay);
} catch (InterruptedException x) {
}
}
if (fCanceled)
break;
if (!fIsDirty)
continue;
if (fReset) {
synchronized (this) {
fReset= false;
}
continue;
}
DirtyRegion r= null;
synchronized (fDirtyRegionQueue) {
r= fDirtyRegionQueue.removeNextDirtyRegion();
}
fIsActive= true;
if (fProgressMonitor != null)
fProgressMonitor.setCanceled(false);
process(r);
synchronized (fDirtyRegionQueue) {
if (0 == fDirtyRegionQueue.getSize()) {
synchronized (this) {
fIsDirty= false;
}
fDirtyRegionQueue.notifyAll();
}
}
fIsActive= false;
}
}
};
/**
* Internal document listener and text input listener.
*/
class Listener implements IDocumentListener, ITextInputListener {
/*
* @see IDocumentListener#documentAboutToBeChanged(DocumentEvent)
*/
public void documentAboutToBeChanged(DocumentEvent e) {
}
/*
* @see IDocumentListener#documentChanged(DocumentEvent)
*/
public void documentChanged(DocumentEvent e) {
if (fProgressMonitor != null && fThread.isActive())
fProgressMonitor.setCanceled(true);
if (fIsIncrementalReconciler)
createDirtyRegion(e);
fThread.reset();
}
/*
* @see ITextInputListener#inputDocumentAboutToBeChanged(IDocument, IDocument)
*/
public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
if (oldInput == fDocument) {
if (fDocument != null)
fDocument.removeDocumentListener(this);
if (fIsIncrementalReconciler) {
fDirtyRegionQueue.purgeQueue();
if (fDocument != null && fDocument.getLength() > 0) {
DocumentEvent e= new DocumentEvent(fDocument, 0, fDocument.getLength(), null);
createDirtyRegion(e);
fThread.suspendCallerWhileDirty();
}
}
fDocument= null;
}
}
/*
* @see ITextInputListener#inputDocumentChanged(IDocument, IDocument)
*/
public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
fDocument= newInput;
if (fDocument == null)
return;
reconcilerDocumentChanged(fDocument);
fDocument.addDocumentListener(this);
if (fIsIncrementalReconciler) {
DocumentEvent e= new DocumentEvent(fDocument, 0, 0, fDocument.get());
createDirtyRegion(e);
}
startReconciling();
}
};
/** Queue to manage the changes applied to the text viewer */
private DirtyRegionQueue fDirtyRegionQueue;
/** The background thread */
private BackgroundThread fThread;
/** Internal document and text input listener */
private Listener fListener;
/** The background thread delay */
private int fDelay= 500;
/** Are there incremental reconciling strategies? */
private boolean fIsIncrementalReconciler= true;
/** The progress monitor used by this reconciler */
private IProgressMonitor fProgressMonitor;
/** The text viewer's document */
private IDocument fDocument;
/** The text viewer */
private ITextViewer fViewer;
/**
* Processes a dirty region. If the dirty region is <code>null</code> the whole
* document is consider being dirty. The dirty region is partitioned by the
* document and each partition is handed over to a reconciling strategy registered
* for the partition's content type.
*
* @param dirtyRegion the dirty region to be processed
*/
abstract protected void process(DirtyRegion dirtyRegion);
/**
* Hook called when the document whose contents should be reconciled
* has been changed, i.e., the input document of the text viewer this
* reconciler is installed on. Usually, subclasses use this hook to
* inform all their reconciling strategies about the change.
*
* @param newDocument the new reconciler document
*/
abstract protected void reconcilerDocumentChanged(IDocument newDocument);
/**
* Creates a new reconciler without configuring it.
*/
protected AbstractReconciler() {
super();
}
/**
* Tells the reconciler how long it should wait for further text changes before
* activating the appropriate reconciling strategies.
*
* @param delay the duration in milli seconds of a change collection period.
*/
public void setDelay(int delay) {
fDelay= delay;
}
/**
* Tells the reconciler whether any of the available reconciling strategies
* is interested in getting detailed dirty region information or just in the
* fact the the document has been changed. In the second case, the reconciling
* can not incrementally be pursued.
*
* @param isIncremental indicates whether this reconciler will be configured with
* incremental reconciling strategies
*
* @see DirtyRegion
* @see IReconcilingStrategy
*/
public void setIsIncrementalReconciler(boolean isIncremental) {
fIsIncrementalReconciler= isIncremental;
}
/**
* Sets the progress monitor of this reconciler.
*
* @param monitor the monitor to be used
*/
public void setProgressMonitor(IProgressMonitor monitor) {
fProgressMonitor= monitor;
}
/**
* Returns whether any of the reconciling strategies is interested in
* detailed dirty region information.
*
* @return whether this reconciler is incremental
*
* @see IReconcilingStrategy
*/
protected boolean isIncrementalReconciler() {
return fIsIncrementalReconciler;
}
/**
* Returns the input document of the text viewer this reconciler is installed on.
*
* @return the reconciler document
*/
protected IDocument getDocument() {
return fDocument;
}
/**
* Returns the text viewer this reconciler is installed on.
*
* @return the text viewer this reconciler is installed on
*/
protected ITextViewer getTextViewer() {
return fViewer;
}
/**
* Returns the progress monitor of this reconciler.
*
* @return the progress monitor of this reconciler
*/
protected IProgressMonitor getProgressMonitor() {
return fProgressMonitor;
}
/*
* @see IReconciler#install(ITextViewer)
*/
public void install(ITextViewer textViewer) {
Assert.isNotNull(textViewer);
fViewer= textViewer;
fListener= new Listener();
fViewer.addTextInputListener(fListener);
fDirtyRegionQueue= new DirtyRegionQueue();
fThread= new BackgroundThread(getClass().getName());
}
/*
* @see IReconciler#uninstall()
*/
public void uninstall() {
if (fListener != null) {
fViewer.removeTextInputListener(fListener);
if (fDocument != null) fDocument.removeDocumentListener(fListener);
fListener= null;
synchronized (this) {
// http://dev.eclipse.org/bugs/show_bug.cgi?id=19135
BackgroundThread bt= fThread;
fThread= null;
bt.cancel();
}
}
}
/**
* Creates a dirty region for a document event and adds it to the queue.
*
* @param e the document event for which to create a dirty region
*/
private void createDirtyRegion(DocumentEvent e) {
if (e.getLength() == 0 && e.getText() != null) {
// Insert
fDirtyRegionQueue.addDirtyRegion(new DirtyRegion(e.getOffset(), e.getText().length(), DirtyRegion.INSERT, e.getText()));
} else if (e.getText() == null || e.getText().length() == 0) {
// Remove
fDirtyRegionQueue.addDirtyRegion(new DirtyRegion(e.getOffset(), e.getLength(), DirtyRegion.REMOVE, null));
} else {
// Replace (Remove + Insert)
fDirtyRegionQueue.addDirtyRegion(new DirtyRegion(e.getOffset(), e.getLength(), DirtyRegion.REMOVE, null));
fDirtyRegionQueue.addDirtyRegion(new DirtyRegion(e.getOffset(), e.getText().length(), DirtyRegion.INSERT, e.getText()));
}
}
/**
* This method is called on startup of the background activity. It is called only
* once during the life time of the reconciler. Clients may reimplement this method.
*/
protected void initialProcess() {
}
/**
* Forces the reconciler to reconcile the structure of the whole document.
* Clients may extend this method.
*/
protected void forceReconciling() {
if (fDocument != null) {
if (fIsIncrementalReconciler) {
DocumentEvent e= new DocumentEvent(fDocument, 0, fDocument.getLength(), fDocument.get());
createDirtyRegion(e);
}
startReconciling();
}
}
/**
* Starts the reconciler to reconcile the queued dirty-regions.
* Clients may extend this method.
*/
protected synchronized void startReconciling() {
if (fThread == null)
return;
if (!fThread.isAlive())
fThread.start();
fThread.reset();
}
/**
* Hook that is called after the reconciler thread has been reset.
*/
protected void reconcilerReset() {
}
}