package org.erlide.ui.editors.internal.reconciling; import java.util.List; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IProgressMonitor; 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; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.reconciler.AbstractReconciler; import org.eclipse.jface.text.reconciler.DirtyRegion; import org.eclipse.jface.text.reconciler.IReconciler; import org.eclipse.jface.text.reconciler.IReconcilingStrategy; import org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension; import org.eclipse.ui.texteditor.ITextEditor; import org.erlide.engine.ErlangEngine; import org.erlide.engine.model.root.IErlModule; import org.erlide.ui.editors.erl.ErlangEditor; import org.erlide.util.ErlLogger; public class ErlReconciler implements IReconciler { private final IErlReconcilingStrategy fStrategy; private final String path; ErlDirtyRegionQueue fDirtyRegionQueue; ReconcilerThread fThread; private Listener fListener; int fDelay = 500; boolean fIsIncrementalReconciler = true; IProgressMonitor fProgressMonitor; boolean fIsAllowedToModifyDocument = true; IDocument fDocument; private ITextViewer fViewer; /** True if it should reconcile all regions without delay between them */ final boolean fChunkReconciler; private Object fMutex; public ErlReconciler(final IErlReconcilingStrategy strategy, final boolean isIncremental, final boolean chunkReconciler, final String path, final IErlModule module, final ITextEditor editor) { super(); Assert.isNotNull(strategy); setIsIncrementalReconciler(isIncremental); fChunkReconciler = chunkReconciler; fStrategy = strategy; this.path = path; if (path != null) { ErlangEngine.getInstance().getModel().putEdited(path, module); } // https://bugs.eclipse.org/bugs/show_bug.cgi?id=63898 if (editor instanceof ErlangEditor) { fMutex = ((ErlangEditor) editor).getReconcilerLock(); } else { fMutex = new Object(); // Null Object } } /** * Background thread for the reconciling activity. */ class ReconcilerThread extends Thread { private static final int RECONCILER_SUSPEND_LOOP_MAX = 10; private boolean fCanceled = false; private boolean fReset = false; private boolean fIsActive = false; public ReconcilerThread(final String name) { super(name); setPriority(Thread.MIN_PRIORITY); setDaemon(true); } public boolean isActive() { return fIsActive; } public boolean isDirty() { synchronized (fDirtyRegionQueue) { return !fDirtyRegionQueue.isEmpty(); } } public void cancel() { fCanceled = true; final IProgressMonitor pm = fProgressMonitor; if (pm != null) { pm.setCanceled(true); } synchronized (fDirtyRegionQueue) { fDirtyRegionQueue.notifyAll(); } } public void suspendCallerWhileDirty() { boolean isDirty = true; int i = RECONCILER_SUSPEND_LOOP_MAX; while (i > 0 && isDirty) { i--; synchronized (fDirtyRegionQueue) { isDirty = isDirty(); if (isDirty) { try { fDirtyRegionQueue.wait(fDelay); } catch (final InterruptedException x) { } } } } if (i == 0 || isDirty) { ErlLogger.debug("broke out of loop i %d isDirty %b", i, isDirty); } } public synchronized void reset() { if (fDelay > 0) { fReset = true; } else { synchronized (fDirtyRegionQueue) { fDirtyRegionQueue.notifyAll(); } } reconcilerReset(); } public synchronized void unreset() { fReset = false; } /** * 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. If * fReconcileAllAtOnce is set, it removes all changes and processes * them. * <p> * Calls {@link AbstractReconciler#initialProcess()} on entrance. * </p> */ @Override public void run() { synchronized (fDirtyRegionQueue) { try { fDirtyRegionQueue.wait(fDelay); } catch (final InterruptedException x) { } } initialProcess(); while (!fCanceled) { synchronized (fDirtyRegionQueue) { try { fDirtyRegionQueue.wait(fDelay); } catch (final InterruptedException x) { } } if (fCanceled) { break; } synchronized (fDirtyRegionQueue) { if (fDirtyRegionQueue.isEmpty()) { continue; } } synchronized (this) { if (fReset) { fReset = false; continue; } } List<ErlDirtyRegion> rs = null; ErlDirtyRegion r = null; synchronized (fDirtyRegionQueue) { if (fChunkReconciler) { rs = fDirtyRegionQueue.extractAllDirtyRegions(); } else { r = fDirtyRegionQueue.extractNextDirtyRegion(); } fDirtyRegionQueue.notifyAll(); } fIsActive = true; if (fProgressMonitor != null) { fProgressMonitor.setCanceled(false); } if (fChunkReconciler) { if (rs != null) { for (final ErlDirtyRegion dirtyRegion : rs) { process(dirtyRegion); } } } else { process(r); } postProcess(); fIsActive = false; } } } class Listener implements IDocumentListener, ITextInputListener { @Override public void documentAboutToBeChanged(final DocumentEvent e) { } @Override public void documentChanged(final DocumentEvent e) { // ErlLogger.debug("documentChanged %d %d %d", e.getOffset(), // e.getLength(), e.getText().length()); if (!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$ } aboutToBeReconciled(); } /* * The second OR condition handles the case when the document gets * changed while still inside initialProcess(). */ if (fProgressMonitor != null && (fThread.isActive() || fThread.isDirty() && fThread.isAlive())) { fProgressMonitor.setCanceled(true); } if (fIsIncrementalReconciler) { createDirtyRegion(e); } fThread.reset(); } @Override public void inputDocumentAboutToBeChanged(final IDocument oldInput, final IDocument newInput) { if (oldInput == fDocument) { if (fDocument != null) { fDocument.removeDocumentListener(this); } if (fIsIncrementalReconciler) { synchronized (fDirtyRegionQueue) { fDirtyRegionQueue.purgeQueue(); } if (fDocument != null && fDocument.getLength() > 0) { // final DocumentEvent e = new DocumentEvent(fDocument, // 0, // fDocument.getLength(), ""); //$NON-NLS-1$ // createDirtyRegion(e); fThread.reset(); fThread.suspendCallerWhileDirty(); } } fDocument = null; } } @Override public void inputDocumentChanged(final IDocument oldInput, final IDocument newInput) { fDocument = newInput; if (fDocument == null) { return; } reconcilerDocumentChanged(fDocument); fDocument.addDocumentListener(this); if (!fThread.isDirty()) { aboutToBeReconciled(); } // if (fIsIncrementalReconciler) { // final DocumentEvent e = new DocumentEvent(fDocument, 0, 0, // fDocument.get()); // createDirtyRegion(e); // } startReconciling(); } } /** * 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(final 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(final 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(final boolean isAllowedToModify) { fIsAllowedToModifyDocument = isAllowedToModify; } protected boolean isIncrementalReconciler() { return fIsIncrementalReconciler; } protected IDocument getDocument() { return fDocument; } protected ITextViewer getTextViewer() { return fViewer; } protected IProgressMonitor getProgressMonitor() { return fProgressMonitor; } @Override public void install(final ITextViewer textViewer) { Assert.isNotNull(textViewer); fViewer = textViewer; synchronized (this) { if (fThread != null) { return; } fThread = new ReconcilerThread(getClass().getName()); } fDirtyRegionQueue = new ErlDirtyRegionQueue(); 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. final 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 final ReconcilerThread bt = fThread; fThread = null; bt.cancel(); } } final ErlReconcilingStrategy s = (ErlReconcilingStrategy) getReconcilingStrategy( IDocument.DEFAULT_CONTENT_TYPE); s.uninstall(); if (path != null) { ErlangEngine.getInstance().getModel().putEdited(path, null); } } protected void createDirtyRegion(final DocumentEvent e) { synchronized (fDirtyRegionQueue) { String text = e.getText(); if (text == null) { text = ""; } final ErlDirtyRegion erlDirtyRegion = new ErlDirtyRegion(e.getOffset(), e.getLength(), text); fDirtyRegionQueue.addDirtyRegion(erlDirtyRegion); fDirtyRegionQueue.notifyAll(); } } /** * 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() { } /** * Forces the reconciler to reconcile the structure of the whole document. * Clients may extend this method. */ public void forceReconciling() { if (fDocument != null) { if (!fThread.isDirty() && fThread.isAlive()) { aboutToBeReconciled(); } if (fProgressMonitor != null && fThread.isActive()) { fProgressMonitor.setCanceled(true); } if (fIsIncrementalReconciler) { final 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 (final 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() { } @Override public IReconcilingStrategy getReconcilingStrategy(final String contentType) { Assert.isNotNull(contentType); return fStrategy; } protected void process(final ErlDirtyRegion dirtyRegion) { if (dirtyRegion != null) { fStrategy.reconcile(dirtyRegion); } else { final IDocument document = getDocument(); if (document != null) { fStrategy.reconcile(new Region(0, document.getLength())); } } } protected void postProcess() { fStrategy.chunkReconciled(); } protected void reconcilerDocumentChanged(final IDocument document) { fStrategy.setDocument(document); } public void setProgressMonitor(final IProgressMonitor monitor) { fProgressMonitor = monitor; if (fStrategy instanceof IReconcilingStrategyExtension) { final IReconcilingStrategyExtension extension = (IReconcilingStrategyExtension) fStrategy; extension.setProgressMonitor(monitor); } } /** * This method is called on startup of the background activity. It is called * only once during the life time of the reconciler. */ protected void initialProcess() { synchronized (fMutex) { if (fStrategy instanceof IReconcilingStrategyExtension) { final IReconcilingStrategyExtension extension = (IReconcilingStrategyExtension) fStrategy; extension.initialReconcile(); } } } public void reconcileNow() { fThread.unreset(); synchronized (fDirtyRegionQueue) { fDirtyRegionQueue.notifyAll(); } fThread.suspendCallerWhileDirty(); } public void reset() { if (fIsIncrementalReconciler) { synchronized (fDirtyRegionQueue) { fDirtyRegionQueue.purgeQueue(); fDirtyRegionQueue.notifyAll(); } fThread.reset(); initialProcess(); } } }