blob: af2fd716179ccfa1bec0dea2afd9f708b56e3d17 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2007, 2019 Stephan Wahlbrink and others.
#
# 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, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.ltk.ui.sourceediting;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextInputListener;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.reconciler.IReconciler;
import org.eclipse.jface.text.reconciler.IReconcilingStrategy;
import org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.statet.jcommons.collections.CopyOnWriteIdentityListSet;
import org.eclipse.statet.jcommons.collections.ImIdentityList;
import org.eclipse.statet.ltk.model.core.elements.ISourceUnit;
/**
* Reconciler for {@link ISourceEditor}s using Eclipse Job API.
*/
public class EcoReconciler2 implements IReconciler {
public static interface ISourceUnitStrategy {
void setInput(ISourceUnit su);
}
protected static class StrategyEntry {
final IReconcilingStrategy strategy;
final IReconcilingStrategyExtension strategyExtension;
final ISourceUnitStrategy suStrategy;
boolean initialed;
StrategyEntry(final IReconcilingStrategy strategy) {
this.strategy= strategy;
this.strategyExtension= (strategy instanceof IReconcilingStrategyExtension) ?
(IReconcilingStrategyExtension) strategy : null;
this.initialed= false;
this.suStrategy= (strategy instanceof ISourceUnitStrategy) ?
(ISourceUnitStrategy) strategy : null;
}
@Override
public int hashCode() {
return this.strategy.hashCode();
}
@Override
public boolean equals(final Object obj) {
if (obj instanceof StrategyEntry) {
return ( ((StrategyEntry) obj).strategy == this.strategy);
}
return false;
}
}
private class ReconcileJob extends Job implements ISchedulingRule {
ReconcileJob(final String name) {
super("Reconciler '"+name+"'"); //$NON-NLS-1$ //$NON-NLS-2$
setPriority(Job.SHORT);
setRule(this);
setSystem(true);
setUser(false);
}
@Override
protected IStatus run(final IProgressMonitor monitor) {
if (!monitor.isCanceled()) {
processReconcile(monitor);
}
return Status.OK_STATUS;
}
@Override
public boolean contains(final ISchedulingRule rule) {
return rule == this;
}
@Override
public boolean isConflicting(final ISchedulingRule rule) {
return rule == this;
}
}
private class VisibleListener implements Listener {
@Override
public void handleEvent(final Event event) {
switch (event.type) {
case SWT.Show:
EcoReconciler2.this.isEditorVisible= true;
return;
case SWT.Hide:
EcoReconciler2.this.isEditorVisible= false;
return;
}
}
}
/**
* Internal document listener and text input listener.
*/
private class DocumentListener implements IDocumentListener, ITextInputListener {
@Override
public void documentAboutToBeChanged(final DocumentEvent e) {
}
@Override
public void documentChanged(final DocumentEvent e) {
scheduleReconcile();
}
@Override
public void inputDocumentAboutToBeChanged(final IDocument oldInput, final IDocument newInput) {
if (EcoReconciler2.this.document != null && oldInput == EcoReconciler2.this.document && newInput != EcoReconciler2.this.document) {
disconnectDocument();
}
}
@Override
public void inputDocumentChanged(final IDocument oldInput, final IDocument newInput) {
connectDocument();
}
}
/** Internal document and text input listener. */
private final DocumentListener documentListener= new DocumentListener();
private VisibleListener visibleListener;
/** Job for scheduled background reconciling */
private ReconcileJob job;
/** The background thread delay. */
private int delay= 500;
/** The text viewer's document. */
private IDocument document;
/** The text viewer */
private ITextViewer viewer;
/** optional editor */
private ISourceEditor editor;
/** Tells whether this reconciler's editor is active. */
private volatile boolean isEditorVisible;
/** The current source unit */
private ISourceUnit sourceUnit;
private final CopyOnWriteIdentityListSet<StrategyEntry> strategies= new CopyOnWriteIdentityListSet<>();
/**
* Creates a new reconciler without configuring it.
*/
public EcoReconciler2() {
super();
}
/**
* Creates a new reconciler without configuring it.
*/
public EcoReconciler2(final ISourceEditor editor) {
super();
this.editor= editor;
}
/**
* 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) {
this.delay= delay;
}
/**
* Returns the input document of the text viewer this reconciler is installed on.
*
* @return the reconciler document
*/
protected IDocument getDocument() {
return this.document;
}
/**
* Returns the text viewer this reconciler is installed on.
*
* @return the text viewer this reconciler is installed on
*/
protected ITextViewer getTextViewer() {
return this.viewer;
}
/**
* Tells whether this reconciler's editor is active.
*
* @return <code>true</code> if the editor is active
*/
protected boolean isEditorVisible() {
return this.isEditorVisible;
}
@Override
public void install(final ITextViewer textViewer) {
Assert.isNotNull(textViewer);
this.viewer= textViewer;
this.visibleListener= new VisibleListener();
final StyledText textWidget= this.viewer.getTextWidget();
textWidget.addListener(SWT.Show, this.visibleListener);
textWidget.addListener(SWT.Hide, this.visibleListener);
this.isEditorVisible= textWidget.isVisible();
this.viewer.addTextInputListener(this.documentListener);
connectDocument();
}
@Override
public void uninstall() {
if (this.viewer != null) {
disconnectDocument();
this.viewer.removeTextInputListener(this.documentListener);
this.viewer= null;
}
}
protected void connectDocument() {
final IDocument document= this.viewer.getDocument();
if (document == null || this.document == document) {
return;
}
this.document= document;
this.sourceUnit= this.editor.getSourceUnit();
reconcilerDocumentChanged(this.document);
this.job= new ReconcileJob(getInputName());
this.document.addDocumentListener(this.documentListener);
scheduleReconcile();
}
protected String getInputName() {
if (this.sourceUnit != null) {
return this.sourceUnit.getId();
}
return "-"; //$NON-NLS-1$
}
/**
* 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
*/
protected void reconcilerDocumentChanged(final IDocument newDocument) {
}
protected void disconnectDocument() {
if (this.document != null) {
this.document.removeDocumentListener(this.documentListener);
this.document= null;
this.sourceUnit= null;
}
if (this.job != null) {
this.job.cancel();
this.job= null;
}
}
private synchronized void scheduleReconcile() {
if ((this.job.getState() & (Job.SLEEPING | Job.WAITING)) == 0) {
aboutToBeReconciled();
}
this.job.cancel();
this.job.schedule(this.delay);
}
/**
* Hook for subclasses which want to perform some
* action as soon as reconciliation is needed.
* <p>
* Default implementation is to do nothing.
*/
protected void aboutToBeReconciled() {
}
protected void processReconcile(final IProgressMonitor monitor) {
final IDocument document= getDocument();
final ISourceUnit input= this.sourceUnit;
if (document == null || input == null) {
return;
}
final IRegion region= new Region(0, document.getLength());
final ImIdentityList<StrategyEntry> reconcilingStrategies= getReconcilingStrategies();
for (final StrategyEntry s : reconcilingStrategies) {
synchronized (s.strategy) {
if (s.suStrategy != null && input != null) {
s.suStrategy.setInput(input);
}
else {
s.strategy.setDocument(document);
}
if (!prepareStrategyReconcile(s)) {
continue;
}
if (monitor.isCanceled()) {
return;
}
if (s.strategyExtension != null) {
s.strategyExtension.setProgressMonitor(monitor);
if (!s.initialed) {
s.strategyExtension.initialReconcile();
s.initialed= true;
continue;
}
}
s.strategy.reconcile(region);
}
}
}
protected boolean prepareStrategyReconcile(final StrategyEntry s) {
return true;
}
public void addReconcilingStrategy(final IReconcilingStrategy strategy) {
this.strategies.add(new StrategyEntry(strategy));
}
protected ImIdentityList<StrategyEntry> getReconcilingStrategies() {
return this.strategies.toList();
}
@Override
public IReconcilingStrategy getReconcilingStrategy(final String contentType) {
return null;
}
}