blob: a31a702ad69ff83f0cf62ad95450cecd118de1a4 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015, 2018 1C-Soft LLC.
*
* This program and the accompanying materials are made available under
* the terms of the Eclipse Public License 2.0 which is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* 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 working copy reconciler with the given working copy manager.
* The working copy manager is used to determine the working copy for
* the reconciler's document. The reconciler is configured with a single
* reconciling strategy (by default, a {@link WorkingCopyReconcilingStrategy})
* that is used irrespective of where a dirty region is located in the
* reconciler's document.
*
* @param workingCopyManager 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 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);
}
}
/**
* {@inheritDoc}
* <p>
* <code>WorkingCopyReconciler</code> extends this method to {@link
* #addElementChangeListener(IElementChangeListener) register} an
* element change listener that {@link #elementChanged(IElementChangeEvent)
* notifies} when a change in the underlying model {@link #isAffectedBy(
* IElementChangeEvent) affects} the reconciler in some way, and also
* to register a shell listener that {@link #setActive(boolean) sets}
* the active state of the reconciler when the reconciler's text viewer
* is activated or deactivated.
* </p>
*/
@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();
}
/**
* {@inheritDoc}
* <p>
* This implementation always returns the single strategy of this reconciler.
* </p>
*/
@Override
public IReconcilingStrategy getReconcilingStrategy(String contentType)
{
return strategy;
}
/**
* {@inheritDoc}
* <p>
* If the reconciling strategy of this reconciler supports
* {@link IReconcilingStrategyExtension}, this implementation
* invokes {@link IReconcilingStrategyExtension#initialReconcile()
* initialReconcile()} on the strategy under the reconciler's
* {@link #getReconcilerLock() lock}.
* </p>
*/
@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 this reconciler. See <a
* href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=66176">Bug 66176</a>
* for a description of the underlying problem.
* <p>
* Default implementation returns <code>this</code>. Subclasses may override.
* </p>
*
* @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 this reconciler is affected in some way
* by the given element change event.
* <p>
* This implementation delegates to {@link #isAffectedBy(IElementDelta,
* ISourceFile)}, passing the working copy for the reconciler's document.
* </p>
*
* @param event never <code>null</code>
* @return <code>true</code> if the reconciler is affected
* by the given element change event, and <code>false</code> otherwise
*/
protected boolean isAffectedBy(IElementChangeEvent event)
{
return isAffectedBy(event.getDeltas(), getWorkingCopy());
}
private boolean isAffectedBy(IElementDelta[] deltas,
ISourceFile workingCopy)
{
for (IElementDelta delta : deltas)
{
if (isAffectedBy(delta, workingCopy))
return true;
}
return false;
}
/**
* Returns whether this reconciler is affected by the given element 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, and <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 this 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 deltas within it) is valid only
* for the duration of the invocation of this method.
* </p>
* <p>
* This implementation schedules a runnable to execute on the UI thread,
* to synchronize with {@link #setActive(boolean)}. The runnable will force
* reconciling if the reconciler is active at that time; it will also record
* the fact that a significant change occurred in the underlying model.
* </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,
* and <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.
* <p>
* This implementation sets the active state of the reconciler to the
* given value. Also, it forces reconciling if a significant model change
* occurred while the reconciler was not active.
* </p>
*
* @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);
}
};
}
}