| /*=============================================================================# |
| # 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; |
| } |
| |
| } |