blob: 28a315d5c3a732b7be4a7c1bd000adc10fe7ddb6 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2007, 2020 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.ecommons.text.ui;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
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.ui.IEditorInput;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.statet.ecommons.collections.FastList;
/**
* Reconciler using Eclipse Job API.
*/
public class EcoReconciler implements IReconciler {
protected static class StrategyEntry {
final IReconcilingStrategy strategy;
final IReconcilingStrategyExtension strategyExtension;
boolean initialed;
StrategyEntry(final IReconcilingStrategy strategy) {
this.strategy= strategy;
this.strategyExtension= (strategy instanceof IReconcilingStrategyExtension) ?
(IReconcilingStrategyExtension) strategy : null;
this.initialed= false;
}
@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:
EcoReconciler.this.fIsEditorVisible= true;
return;
case SWT.Hide:
EcoReconciler.this.fIsEditorVisible= 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 (EcoReconciler.this.fDocument != null && oldInput == EcoReconciler.this.fDocument && newInput != EcoReconciler.this.fDocument) {
disconnectDocument();
}
}
@Override
public void inputDocumentChanged(final IDocument oldInput, final IDocument newInput) {
connectDocument();
}
}
/** Internal document and text input listener. */
private final DocumentListener fDocumentListener= new DocumentListener();
private VisibleListener fVisibleListener;
/** Job for scheduled background reconciling */
private ReconcileJob fJob;
/** The background thread delay. */
private int fDelay= 500;
/** The text viewer's document. */
private IDocument fDocument;
/** The text viewer */
private ITextViewer fViewer;
/** optional editor */
private ITextEditor fEditor;
private IEditorInput fEditorInput;
/** Tells whether this reconciler's editor is active. */
private volatile boolean fIsEditorVisible;
private final FastList<StrategyEntry> fStrategies= new FastList<>(StrategyEntry.class, ListenerList.EQUALITY);
/**
* Creates a new reconciler without configuring it.
*/
public EcoReconciler() {
super();
}
/**
* Creates a new reconciler without configuring it.
*/
public EcoReconciler(final ITextEditor editor) {
super();
this.fEditor= 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.fDelay= delay;
}
/**
* Returns the input document of the text viewer this reconciler is installed on.
*
* @return the reconciler document
*/
protected IDocument getDocument() {
return this.fDocument;
}
protected IEditorInput getEditorInput() {
return this.fEditorInput;
}
/**
* Returns the text viewer this reconciler is installed on.
*
* @return the text viewer this reconciler is installed on
*/
protected ITextViewer getTextViewer() {
return this.fViewer;
}
/**
* Tells whether this reconciler's editor is active.
*
* @return <code>true</code> if the editor is active
*/
protected boolean isEditorVisible() {
return this.fIsEditorVisible;
}
@Override
public void install(final ITextViewer textViewer) {
Assert.isNotNull(textViewer);
this.fViewer= textViewer;
this.fVisibleListener= new VisibleListener();
final StyledText textWidget= this.fViewer.getTextWidget();
textWidget.addListener(SWT.Show, this.fVisibleListener);
textWidget.addListener(SWT.Hide, this.fVisibleListener);
this.fIsEditorVisible= textWidget.isVisible();
this.fViewer.addTextInputListener(this.fDocumentListener);
connectDocument();
}
@Override
public void uninstall() {
if (this.fViewer != null) {
disconnectDocument();
this.fViewer.removeTextInputListener(this.fDocumentListener);
this.fViewer= null;
}
}
protected void connectDocument() {
final IDocument document= this.fViewer.getDocument();
if (document == null || this.fDocument == document) {
return;
}
this.fDocument= document;
this.fEditorInput= (this.fEditor != null) ? this.fEditor.getEditorInput() : null;
reconcilerDocumentChanged(this.fDocument);
this.fJob= new ReconcileJob(getInputName());
this.fDocument.addDocumentListener(this.fDocumentListener);
scheduleReconcile();
}
protected String getInputName() {
if (this.fEditorInput != null) {
return this.fEditorInput.getName();
}
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.fDocument != null) {
this.fDocument.removeDocumentListener(this.fDocumentListener);
this.fDocument= null;
this.fEditorInput= null;
}
if (this.fJob != null) {
this.fJob.cancel();
this.fJob= null;
}
}
private synchronized void scheduleReconcile() {
if ((this.fJob.getState() & (Job.SLEEPING | Job.WAITING)) == 0) {
aboutToBeReconciled();
}
this.fJob.cancel();
this.fJob.schedule(this.fDelay);
}
/**
* 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 IEditorInput input= getEditorInput();
if (document == null || (this.fEditor != null && input == null)) {
return;
}
final IRegion region= new Region(0, document.getLength());
final StrategyEntry[] reconcilingStrategies= getReconcilingStrategies();
for (final StrategyEntry s : reconcilingStrategies) {
synchronized (s.strategy) {
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.fStrategies.add(new StrategyEntry(strategy));
}
protected StrategyEntry[] getReconcilingStrategies() {
return this.fStrategies.toArray();
}
@Override
public IReconcilingStrategy getReconcilingStrategy(final String contentType) {
return null;
}
}