blob: e37f7524f2b6f6e5f5de4416a25fb64edbb9ed92 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015, 2016 1C-Soft LLC.
* 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:
* Vladimir Piskarev (1C) - initial API and implementation
*******************************************************************************/
package org.eclipse.handly.ui.text.reconciler;
import static org.eclipse.handly.model.IElementDeltaConstants.F_CHILDREN;
import static org.eclipse.handly.model.IElementDeltaConstants.F_MARKERS;
import static org.eclipse.handly.model.IElementDeltaConstants.F_SYNC;
import static org.eclipse.handly.model.IElementDeltaConstants.F_UNDERLYING_RESOURCE;
import static org.eclipse.handly.model.IElementDeltaConstants.F_WORKING_COPY;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IMarkerDelta;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.handly.model.ElementDeltas;
import org.eclipse.handly.model.IElement;
import org.eclipse.handly.model.IElementChangeEvent;
import org.eclipse.handly.model.IElementChangeListener;
import org.eclipse.handly.model.IElementDelta;
import org.eclipse.handly.model.ISourceFile;
import org.eclipse.handly.ui.IWorkingCopyManager;
import org.eclipse.jface.text.IDocument;
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.IReconcilingStrategy;
import org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension;
import org.eclipse.swt.events.ShellAdapter;
import org.eclipse.swt.events.ShellEvent;
import org.eclipse.swt.events.ShellListener;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;
/**
* An abstract base class of a working copy reconciler that is activated on
* viewer activation and forces reconciling on a significant change in the
* underlying model.
*/
public abstract class WorkingCopyReconciler
extends AbstractReconciler
{
private IWorkingCopyManager workingCopyManager;
private IReconcilingStrategy strategy;
private volatile ISourceFile workingCopy;
private volatile boolean active = true;
private volatile boolean modelChanged = false;
private volatile boolean initialProcessDone = false;
private final IElementChangeListener elementChangeListener =
new IElementChangeListener()
{
@Override
public void elementChanged(IElementChangeEvent event)
{
if (isRunningInReconcilerThread())
return;
if (isAffectedBy(event))
WorkingCopyReconciler.this.elementChanged(event);
}
};
private ShellListener activationListener;
/**
* Creates a new reconciler that reconciles the working copy provided
* by the given manager.
*
* @param workingCopyManager the working copy manager (not <code>null</code>)
*/
public WorkingCopyReconciler(IWorkingCopyManager workingCopyManager)
{
if (workingCopyManager == null)
throw new IllegalArgumentException();
this.workingCopyManager = workingCopyManager;
// Just some reasonable defaults that can be overwritten:
setIsIncrementalReconciler(false);
setIsAllowedToModifyDocument(false);
setReconcilingStrategy(new WorkingCopyReconcilingStrategy(
workingCopyManager));
}
/**
* Sets the reconciling strategy that is to be used by this reconciler.
*
* @param strategy the reconciling strategy (not <code>null</code>)
*/
public void setReconcilingStrategy(IReconcilingStrategy strategy)
{
if (strategy == null)
throw new IllegalArgumentException();
this.strategy = strategy;
if (strategy instanceof IReconcilingStrategyExtension)
{
IReconcilingStrategyExtension extension =
(IReconcilingStrategyExtension)strategy;
extension.setProgressMonitor(getProgressMonitor());
}
}
@Override
public void setProgressMonitor(IProgressMonitor monitor)
{
super.setProgressMonitor(monitor);
if (strategy instanceof IReconcilingStrategyExtension)
{
IReconcilingStrategyExtension extension =
(IReconcilingStrategyExtension)strategy;
extension.setProgressMonitor(monitor);
}
}
@Override
public void install(ITextViewer textViewer)
{
super.install(textViewer);
setWorkingCopy(workingCopyManager.getWorkingCopy(
textViewer.getDocument()));
addElementChangeListener(elementChangeListener);
Control control = textViewer.getTextWidget();
activationListener = new ActivationListener(control);
control.getShell().addShellListener(activationListener);
}
@Override
public void uninstall()
{
Control control = getTextViewer().getTextWidget();
if (!control.isDisposed())
control.getShell().removeShellListener(activationListener);
activationListener = null;
removeElementChangeListener(elementChangeListener);
setWorkingCopy(null);
super.uninstall();
}
@Override
public IReconcilingStrategy getReconcilingStrategy(String contentType)
{
return strategy;
}
@Override
protected void initialProcess()
{
synchronized (getReconcilerLock())
{
if (strategy instanceof IReconcilingStrategyExtension)
{
IReconcilingStrategyExtension extension =
(IReconcilingStrategyExtension)strategy;
extension.initialReconcile();
}
}
initialProcessDone = true;
}
@Override
protected void process(DirtyRegion dirtyRegion)
{
if (dirtyRegion != null)
strategy.reconcile(dirtyRegion, dirtyRegion);
else
{
IDocument document = getDocument();
if (document != null)
strategy.reconcile(new Region(0, document.getLength()));
}
}
@Override
protected void forceReconciling()
{
if (!initialProcessDone)
return;
super.forceReconciling();
}
@Override
protected void reconcilerDocumentChanged(IDocument newDocument)
{
setWorkingCopy(workingCopyManager.getWorkingCopy(newDocument));
strategy.setDocument(newDocument);
}
/**
* Returns the mutex for the reconciler.
* See https://bugs.eclipse.org/bugs/show_bug.cgi?id=66176
* for a description of the underlying problem.
*
* @return the mutex for the reconciler (never <code>null</code>)
*/
protected Object getReconcilerLock()
{
return this;
}
/**
* Registers the given element change listener with the underlying model.
*
* @param listener never <code>null</code>
*/
protected abstract void addElementChangeListener(
IElementChangeListener listener);
/**
* Removes the given element change listener from the underlying model.
*
* @param listener never <code>null</code>
*/
protected abstract void removeElementChangeListener(
IElementChangeListener listener);
/**
* Returns whether the reconciler is affected in some way
* by the given element change event.
*
* @param event never <code>null</code>
* @return <code>true</code> if the reconciler is affected
* by the given element change event, <code>false</code> otherwise
*/
protected boolean isAffectedBy(IElementChangeEvent event)
{
return isAffectedBy(event.getDelta(), getWorkingCopy());
}
/**
* Returns whether the reconciler is affected by the given delta
* with regard to the given working copy.
*
* @param delta never <code>null</code>
* @param workingCopy may be <code>null</code>
* @return <code>true</code> if the reconciler is affected
* by the given delta, <code>false</code> otherwise
*/
protected boolean isAffectedBy(IElementDelta delta, ISourceFile workingCopy)
{
long flags = ElementDeltas.getFlags(delta);
if (flags == F_SYNC || flags == F_WORKING_COPY)
return false;
IElement element = ElementDeltas.getElement(delta);
if (flags == F_UNDERLYING_RESOURCE && element.equals(workingCopy))
return false; // saving this reconciler's working copy
if (flags == F_MARKERS)
{
if (element.equals(workingCopy))
{
for (IMarkerDelta markerDelta : ElementDeltas.getMarkerDeltas(
delta))
{
if (markerDelta.isSubtypeOf(IMarker.PROBLEM))
return true;
}
}
return false;
}
if (flags != F_CHILDREN)
return true;
for (IElementDelta child : ElementDeltas.getAffectedChildren(delta))
{
if (isAffectedBy(child, workingCopy))
return true;
}
return false;
}
/**
* Notifies that the reconciler is affected in some way
* by the given element change event.
* <p>
* <b>Note</b> This method may be called in any thread.
* The event object (and the delta within it) is valid only
* for the duration of the invocation of this method.
* </p>
*
* @param event never <code>null</code>
*/
protected void elementChanged(IElementChangeEvent event)
{
// run on the UI thread to synchronize with #setActive
PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable()
{
public void run()
{
setModelChanged(true);
if (isActive())
forceReconciling();
}
});
}
/**
* Returns whether this reconciler is currently active.
*
* @return <code>true</code> if this reconciler is currently active,
* <code>false</code> otherwise
*/
protected boolean isActive()
{
return active;
}
/**
* Indicates a change in the active state of this reconciler.
* This method can only be executed by the UI thread.
*
* @param active the boolean value to set for the reconciler active state
*/
protected void setActive(boolean active)
{
this.active = active;
if (Display.getCurrent() == null)
throw new AssertionError(
"This method may only be executed by the user-interface thread"); //$NON-NLS-1$
if (!active)
setModelChanged(false);
else if (hasModelChanged())
forceReconciling();
}
private ISourceFile getWorkingCopy()
{
return workingCopy;
}
private void setWorkingCopy(ISourceFile workingCopy)
{
this.workingCopy = workingCopy;
}
private boolean hasModelChanged()
{
return modelChanged;
}
private void setModelChanged(boolean modelChanged)
{
this.modelChanged = modelChanged;
}
private class ActivationListener
extends ShellAdapter
{
private final Control control;
ActivationListener(Control control)
{
if (control == null)
throw new IllegalArgumentException();
this.control = control;
}
@Override
public void shellActivated(ShellEvent e)
{
if (!control.isDisposed() && control.isVisible())
{
setActive(true);
}
};
@Override
public void shellDeactivated(ShellEvent e)
{
if (!control.isDisposed() && control.getShell() == e.getSource())
{
setActive(false);
}
};
}
}