/*=============================================================================# # Copyright (c) 2007-2016 Stephan Wahlbrink (WalWare.de) and others. # 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: # Stephan Wahlbrink - initial API and implementation #=============================================================================*/ package de.walware.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 de.walware.ecommons.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 strategy.hashCode(); } @Override public boolean equals(final Object obj) { if (obj instanceof StrategyEntry) { return ( ((StrategyEntry) obj).strategy == 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: fIsEditorVisible = true; return; case SWT.Hide: 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 (fDocument != null && oldInput == fDocument && newInput != 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(); 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) { fDelay = delay; } /** * Returns the input document of the text viewer this reconciler is installed on. * * @return the reconciler document */ protected IDocument getDocument() { return fDocument; } protected IEditorInput getEditorInput() { return fEditorInput; } /** * Returns the text viewer this reconciler is installed on. * * @return the text viewer this reconciler is installed on */ protected ITextViewer getTextViewer() { return fViewer; } /** * Tells whether this reconciler's editor is active. * * @return <code>true</code> if the editor is active */ protected boolean isEditorVisible() { return fIsEditorVisible; } @Override public void install(final ITextViewer textViewer) { Assert.isNotNull(textViewer); fViewer = textViewer; fVisibleListener = new VisibleListener(); final StyledText textWidget = fViewer.getTextWidget(); textWidget.addListener(SWT.Show, fVisibleListener); textWidget.addListener(SWT.Hide, fVisibleListener); fIsEditorVisible = textWidget.isVisible(); fViewer.addTextInputListener(fDocumentListener); connectDocument(); } @Override public void uninstall() { if (fViewer != null) { disconnectDocument(); fViewer.removeTextInputListener(fDocumentListener); fViewer = null; } } protected void connectDocument() { final IDocument document = fViewer.getDocument(); if (document == null || fDocument == document) { return; } fDocument = document; fEditorInput = (fEditor != null) ? fEditor.getEditorInput() : null; reconcilerDocumentChanged(fDocument); fJob = new ReconcileJob(getInputName()); fDocument.addDocumentListener(fDocumentListener); scheduleReconcile(); } protected String getInputName() { if (fEditorInput != null) { return 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 (fDocument != null) { fDocument.removeDocumentListener(fDocumentListener); fDocument = null; fEditorInput = null; } if (fJob != null) { fJob.cancel(); fJob = null; } } private synchronized void scheduleReconcile() { if ((fJob.getState() & (Job.SLEEPING | Job.WAITING)) == 0) { aboutToBeReconciled(); } fJob.cancel(); fJob.schedule(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 || (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) { fStrategies.add(new StrategyEntry(strategy)); } protected StrategyEntry[] getReconcilingStrategies() { return fStrategies.toArray(); } @Override public IReconcilingStrategy getReconcilingStrategy(final String contentType) { return null; } }