diff dwtx/jface/text/reconciler/AbstractReconciler.d @ 129:eb30df5ca28b

Added JFace Text sources
author Frank Benoit <benoit@tionex.de>
date Sat, 23 Aug 2008 19:10:48 +0200
parents
children c4fb132a086c
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dwtx/jface/text/reconciler/AbstractReconciler.d	Sat Aug 23 19:10:48 2008 +0200
@@ -0,0 +1,609 @@
+/*******************************************************************************
+ * Copyright (c) 2000, 2008 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     IBM Corporation - initial API and implementation
+ * Port to the D programming language:
+ *     Frank Benoit <benoit@tionex.de>
+ *******************************************************************************/
+module dwtx.jface.text.reconciler.AbstractReconciler;
+
+import dwt.dwthelper.utils;
+
+
+import dwtx.core.runtime.Assert;
+import dwtx.core.runtime.IProgressMonitor;
+import dwtx.core.runtime.NullProgressMonitor;
+import dwtx.jface.text.DocumentEvent;
+import dwtx.jface.text.IDocument;
+import dwtx.jface.text.IDocumentListener;
+import dwtx.jface.text.ITextInputListener;
+import dwtx.jface.text.ITextViewer;
+
+
+/**
+ * Abstract implementation of {@link IReconciler}. 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 the
+ * {@link #install(ITextViewer)} method.  As a first step {@link #initialProcess()} is
+ * executed in the background. Then, the reconciling thread waits for changes that
+ * need to be reconciled. A reconciler can be resumed by calling {@link #forceReconciling()}
+ * independent from the existence of actual changes. This mechanism is for subclasses only.
+ * It is the clients responsibility to stop a reconciler using its {@link #uninstall()}
+ * method. Unstopped reconcilers do not free their resources.
+ * <p>
+ * It is subclass responsibility to specify how dirty regions are processed.
+ * </p>
+ *
+ * @see dwtx.jface.text.IDocumentListener
+ * @see dwtx.jface.text.ITextInputListener
+ * @see dwtx.jface.text.reconciler.DirtyRegion
+ * @since 2.0
+ */
+abstract public class AbstractReconciler : IReconciler {
+
+
+    /**
+     * Background thread for the reconciling activity.
+     */
+    class BackgroundThread : Thread {
+
+        /** Has the reconciler been canceled. */
+        private bool fCanceled= false;
+        /** Has the reconciler been reset. */
+        private bool fReset= false;
+        /** Some changes need to be processed. */
+        private bool fIsDirty= false;
+        /** Is a reconciling strategy active. */
+        private bool 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 bool isActive() {
+            return fIsActive;
+        }
+
+        /**
+         * Returns whether some changes need to be processed.
+         *
+         * @return <code>true</code> if changes wait to be processed
+         * @since 3.0
+         */
+        public synchronized bool isDirty() {
+            return fIsDirty;
+        }
+
+        /**
+         * Cancels the background thread.
+         */
+        public void cancel() {
+            fCanceled= true;
+            IProgressMonitor pm= fProgressMonitor;
+            if (pm !is null)
+                pm.setCanceled(true);
+            synchronized (fDirtyRegionQueue) {
+                fDirtyRegionQueue.notifyAll();
+            }
+        }
+
+        /**
+         * Suspends the caller of this method until this background thread has
+         * emptied the dirty region queue.
+         */
+        public void suspendCallerWhileDirty() {
+            bool isDirty;
+            do {
+                synchronized (fDirtyRegionQueue) {
+                    isDirty= fDirtyRegionQueue.getSize() > 0;
+                    if (isDirty) {
+                        try {
+                            fDirtyRegionQueue.wait();
+                        } catch (InterruptedException x) {
+                        }
+                    }
+                }
+            } while (isDirty);
+        }
+
+        /**
+         * 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 {@link AbstractReconciler#initialProcess()} on entrance.
+         * </p>
+         */
+        public void run() {
+
+            synchronized (fDirtyRegionQueue) {
+                try {
+                    fDirtyRegionQueue.wait(fDelay);
+                } catch (InterruptedException x) {
+                }
+            }
+
+            if (fCanceled)
+                return;
+
+            initialProcess();
+
+            while (!fCanceled) {
+
+                synchronized (fDirtyRegionQueue) {
+                    try {
+                        fDirtyRegionQueue.wait(fDelay);
+                    } catch (InterruptedException x) {
+                    }
+                }
+
+                if (fCanceled)
+                    break;
+
+                if (!isDirty())
+                    continue;
+
+                synchronized (this) {
+                    if (fReset) {
+                        fReset= false;
+                        continue;
+                    }
+                }
+
+                DirtyRegion r= null;
+                synchronized (fDirtyRegionQueue) {
+                    r= fDirtyRegionQueue.removeNextDirtyRegion();
+                }
+
+                fIsActive= true;
+
+                fProgressMonitor.setCanceled(false);
+
+                process(r);
+
+                synchronized (fDirtyRegionQueue) {
+                    if (0 is fDirtyRegionQueue.getSize()) {
+                        synchronized (this) {
+                            fIsDirty= fProgressMonitor.isCanceled();
+                        }
+                        fDirtyRegionQueue.notifyAll();
+                    }
+                }
+
+                fIsActive= false;
+            }
+        }
+    }
+
+    /**
+     * Internal document listener and text input listener.
+     */
+    class Listener : IDocumentListener, ITextInputListener {
+
+        /*
+         * @see IDocumentListener#documentAboutToBeChanged(DocumentEvent)
+         */
+        public void documentAboutToBeChanged(DocumentEvent e) {
+        }
+
+        /*
+         * @see IDocumentListener#documentChanged(DocumentEvent)
+         */
+        public void documentChanged(DocumentEvent e) {
+
+            if (!fThread.isDirty() && fThread.isAlive()) {
+                if (!fIsAllowedToModifyDocument && Thread.currentThread() is fThread)
+                    throw new UnsupportedOperationException("The reconciler thread is not allowed to modify the document"); //$NON-NLS-1$
+                aboutToBeReconciled();
+            }
+
+            /*
+             * The second OR condition handles the case when the document
+             * gets changed while still inside initialProcess().
+             */
+            if (fThread.isActive() || fThread.isDirty() && fThread.isAlive())
+                fProgressMonitor.setCanceled(true);
+
+            if (fIsIncrementalReconciler)
+                createDirtyRegion(e);
+
+            fThread.reset();
+
+        }
+
+        /*
+         * @see ITextInputListener#inputDocumentAboutToBeChanged(IDocument, IDocument)
+         */
+        public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
+
+            if (oldInput is fDocument) {
+
+                if (fDocument !is null)
+                    fDocument.removeDocumentListener(this);
+
+                if (fIsIncrementalReconciler) {
+                    synchronized (fDirtyRegionQueue) {
+                        fDirtyRegionQueue.purgeQueue();
+                    }
+                    if (fDocument !is null && fDocument.getLength() > 0 && fThread.isDirty() && fThread.isAlive()) {
+                        DocumentEvent e= new DocumentEvent(fDocument, 0, fDocument.getLength(), ""); //$NON-NLS-1$
+                        createDirtyRegion(e);
+                        fThread.reset();
+                        fThread.suspendCallerWhileDirty();
+                    }
+                }
+
+                fDocument= null;
+            }
+        }
+
+        /*
+         * @see ITextInputListener#inputDocumentChanged(IDocument, IDocument)
+         */
+        public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
+
+            fDocument= newInput;
+            if (fDocument is null)
+                return;
+
+
+            reconcilerDocumentChanged(fDocument);
+
+            fDocument.addDocumentListener(this);
+
+            if (!fThread.isDirty())
+                aboutToBeReconciled();
+
+            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 bool fIsIncrementalReconciler= true;
+    /** The progress monitor used by this reconciler. */
+    private IProgressMonitor fProgressMonitor;
+    /**
+     * Tells whether this reconciler is allowed to modify the document.
+     * @since 3.2
+     */
+    private bool fIsAllowedToModifyDocument= true;
+
+
+    /** 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() {
+        fProgressMonitor= new NullProgressMonitor();
+    }
+
+    /**
+     * Tells the reconciler how long it should wait for further text changes before
+     * activating the appropriate reconciling strategies.
+     *
+     * @param delay the duration in milliseconds 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 that 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(bool isIncremental) {
+        fIsIncrementalReconciler= isIncremental;
+    }
+    
+    /**
+     * Tells the reconciler whether it is allowed to change the document
+     * inside its reconciler thread.
+     * <p>
+     * If this is set to <code>false</code> an {@link UnsupportedOperationException}
+     * will be thrown when this restriction will be violated.
+     * </p>
+     *
+     * @param isAllowedToModify indicates whether this reconciler is allowed to modify the document
+     * @since 3.2
+     */
+    public void setIsAllowedToModifyDocument(bool isAllowedToModify) {
+        fIsAllowedToModifyDocument= isAllowedToModify;
+    }
+
+    /**
+     * Sets the progress monitor of this reconciler.
+     *
+     * @param monitor the monitor to be used
+     */
+    public void setProgressMonitor(IProgressMonitor monitor) {
+        Assert.isLegal(monitor !is null);
+        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 bool 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;
+
+        synchronized (this) {
+            if (fThread !is null)
+                return;
+            fThread= new BackgroundThread(getClass().getName());
+        }
+
+        fDirtyRegionQueue= new DirtyRegionQueue();
+
+        fListener= new Listener();
+        fViewer.addTextInputListener(fListener);
+
+        // see bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=67046
+        // if the reconciler gets installed on a viewer that already has a document
+        // (e.g. when reusing editors), we force the listener to register
+        // itself as document listener, because there will be no input change
+        // on the viewer.
+        // In order to do that, we simulate an input change.
+        IDocument document= textViewer.getDocument();
+        if (document !is null) {
+            fListener.inputDocumentAboutToBeChanged(fDocument, document);
+            fListener.inputDocumentChanged(fDocument, document);
+        }
+    }
+
+    /*
+     * @see IReconciler#uninstall()
+     */
+    public void uninstall() {
+        if (fListener !is null) {
+
+            fViewer.removeTextInputListener(fListener);
+            if (fDocument !is null) {
+                fListener.inputDocumentAboutToBeChanged(fDocument, null);
+                fListener.inputDocumentChanged(fDocument, null);
+            }
+            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) {
+        synchronized (fDirtyRegionQueue) {
+            if (e.getLength() is 0 && e.getText() !is null) {
+                // Insert
+                fDirtyRegionQueue.addDirtyRegion(new DirtyRegion(e.getOffset(), e.getText().length(), DirtyRegion.INSERT, e.getText()));
+    
+            } else if (e.getText() is null || e.getText().length() is 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()));
+            }
+        }
+    }
+
+    /**
+     * Hook for subclasses which want to perform some
+     * action as soon as reconciliation is needed.
+     * <p>
+     * Default implementation is to do nothing.
+     * </p>
+     *
+     * @since 3.0
+     */
+    protected void aboutToBeReconciled() {
+    }
+
+    /**
+     * 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 !is null) {
+
+            if (!fThread.isDirty()&& fThread.isAlive())
+                aboutToBeReconciled();
+
+            if (fThread.isActive())
+                fProgressMonitor.setCanceled(true);
+            
+            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 is null)
+            return;
+
+        if (!fThread.isAlive()) {
+            try {
+                fThread.start();
+            } catch (IllegalThreadStateException e) {
+                // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=40549
+                // This is the only instance where the thread is started; since
+                // we checked that it is not alive, it must be dead already due
+                // to a run-time exception or error. Exit.
+            }
+        } else {
+            fThread.reset();
+        }
+    }
+
+    /**
+     * Hook that is called after the reconciler thread has been reset.
+     */
+    protected void reconcilerReset() {
+    }
+    
+    /**
+     * Tells whether the code is running in this reconciler's
+     * background thread.
+     * 
+     * @return <code>true</code> if running in this reconciler's background thread
+     * @since 3.4
+     */
+    protected bool isRunningInReconcilerThread() {
+        return Thread.currentThread() is fThread;
+    }
+}