| /******************************************************************************* |
| * Copyright (c) 2000, 2011 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.jface.text.reconciler; |
| |
| import org.eclipse.core.runtime.Assert; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.NullProgressMonitor; |
| |
| 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 {@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 org.eclipse.jface.text.IDocumentListener |
| * @see org.eclipse.jface.text.ITextInputListener |
| * @see org.eclipse.jface.text.reconciler.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; |
| /** Some changes need to be processed. */ |
| 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; |
| } |
| |
| /** |
| * Returns whether some changes need to be processed. |
| * |
| * @return <code>true</code> if changes wait to be processed |
| * @since 3.0 |
| */ |
| public synchronized boolean isDirty() { |
| return fIsDirty; |
| } |
| |
| /** |
| * Cancels the background thread. |
| */ |
| public void cancel() { |
| fCanceled= true; |
| IProgressMonitor pm= fProgressMonitor; |
| if (pm != 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() { |
| AbstractReconciler.this.signalWaitForFinish(); |
| boolean 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; |
| } |
| synchronized (fDirtyRegionQueue) { |
| fDirtyRegionQueue.notifyAll(); // wake up wait(fDelay); |
| } |
| |
| } else { |
| |
| synchronized (this) { |
| fIsDirty= true; |
| } |
| |
| synchronized (fDirtyRegionQueue) { |
| fDirtyRegionQueue.notifyAll(); |
| } |
| } |
| |
| informNotFinished(); |
| 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> |
| */ |
| @Override |
| public void run() { |
| |
| delay(); |
| |
| if (fCanceled) |
| return; |
| |
| initialProcess(); |
| |
| while (!fCanceled) { |
| |
| delay(); |
| |
| if (fCanceled) |
| break; |
| |
| if (!isDirty()) { |
| waitFinish= false; //signalWaitForFinish() was called but nothing todo |
| 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 == fDirtyRegionQueue.getSize()) { |
| synchronized (this) { |
| fIsDirty= fProgressMonitor.isCanceled(); |
| } |
| fDirtyRegionQueue.notifyAll(); |
| } |
| } |
| |
| fIsActive= false; |
| } |
| } |
| } |
| |
| /** |
| * Internal document listener and text input listener. |
| */ |
| class Listener implements IDocumentListener, ITextInputListener { |
| |
| @Override |
| public void documentAboutToBeChanged(DocumentEvent e) { |
| } |
| |
| @Override |
| public void documentChanged(DocumentEvent e) { |
| |
| if (fThread.isActive() || !fThread.isDirty() && fThread.isAlive()) { |
| if (!fIsAllowedToModifyDocument && Thread.currentThread() == fThread) |
| throw new UnsupportedOperationException("The reconciler thread is not allowed to modify the document"); //$NON-NLS-1$ |
| aboutToBeReconciledInternal(); |
| } |
| |
| /* |
| * 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(); |
| |
| } |
| |
| @Override |
| public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) { |
| |
| if (oldInput == fDocument) { |
| |
| if (fDocument != null) |
| fDocument.removeDocumentListener(this); |
| |
| if (fIsIncrementalReconciler) { |
| synchronized (fDirtyRegionQueue) { |
| fDirtyRegionQueue.purgeQueue(); |
| } |
| if (fDocument != 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; |
| } |
| } |
| |
| @Override |
| public void inputDocumentChanged(IDocument oldInput, IDocument newInput) { |
| |
| fDocument= newInput; |
| if (fDocument == null) |
| return; |
| |
| |
| reconcilerDocumentChanged(fDocument); |
| |
| fDocument.addDocumentListener(this); |
| |
| if (!fThread.isDirty()) |
| aboutToBeReconciledInternal(); |
| |
| 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; |
| /** Signal that the the background thread should not delay. */ |
| volatile boolean waitFinish; |
| /** Are there incremental reconciling strategies? */ |
| private boolean 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 boolean 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(boolean 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(boolean 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 != 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 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; |
| } |
| |
| @Override |
| public void install(ITextViewer textViewer) { |
| |
| Assert.isNotNull(textViewer); |
| fViewer= textViewer; |
| |
| synchronized (this) { |
| if (fThread != 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 != null) { |
| fListener.inputDocumentAboutToBeChanged(fDocument, document); |
| fListener.inputDocumentChanged(fDocument, document); |
| } |
| } |
| |
| @Override |
| public void uninstall() { |
| if (fListener != null) { |
| |
| fViewer.removeTextInputListener(fListener); |
| if (fDocument != 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() == 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().isEmpty()) { |
| // 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() { |
| } |
| |
| /** |
| * Hook for subclasses which want to perform some action as soon as the reconciler starts work |
| * (initial or reconciling) or waiting. |
| * <p> |
| * Default implementation is to do nothing. Implementors may call |
| * {@link #signalWaitForFinish()}. |
| * </p> |
| * |
| * @since 3.20 |
| * @see #signalWaitForFinish |
| */ |
| protected void aboutToWork() { |
| } |
| |
| /** |
| * Signal reconciling should finish as soon as possible. |
| * |
| * @since 3.20 |
| * @see #aboutToWork |
| */ |
| public void signalWaitForFinish() { |
| synchronized (fDirtyRegionQueue) { |
| waitFinish= true; |
| fDirtyRegionQueue.notifyAll(); // notify AbstractReconciler#delay about waitFinish |
| } |
| } |
| |
| private void informNotFinished() { |
| waitFinish= false; |
| aboutToWork(); |
| } |
| |
| private void aboutToBeReconciledInternal() { |
| aboutToBeReconciled(); |
| informNotFinished(); |
| } |
| |
| |
| private void delay() { |
| synchronized (fDirtyRegionQueue) { |
| if (waitFinish) { |
| return; // do not delay when waiting; |
| } |
| try { |
| fDirtyRegionQueue.wait(fDelay); |
| } catch (InterruptedException x) { |
| } |
| } |
| } |
| |
| /** |
| * 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 (!fThread.isDirty()&& fThread.isAlive()) |
| aboutToBeReconciledInternal(); |
| |
| 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 == 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 boolean isRunningInReconcilerThread() { |
| return Thread.currentThread() == fThread; |
| } |
| } |