| /******************************************************************************* |
| * Copyright (c) 2000, 2017 IBM Corporation 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 |
| * |
| *******************************************************************************/ |
| package org.eclipse.dltk.internal.ui.editor; |
| |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.NullProgressMonitor; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.dltk.ast.parser.IModuleDeclaration; |
| import org.eclipse.dltk.core.IModelElement; |
| import org.eclipse.dltk.core.ISourceModule; |
| import org.eclipse.dltk.core.SourceParserUtil; |
| import org.eclipse.dltk.internal.ui.text.ScriptWordFinder; |
| import org.eclipse.dltk.ui.DLTKUIPlugin; |
| import org.eclipse.dltk.ui.PreferenceConstants; |
| import org.eclipse.dltk.ui.search.IOccurrencesFinder; |
| import org.eclipse.dltk.ui.search.IOccurrencesFinder.OccurrenceLocation; |
| import org.eclipse.dltk.ui.viewsupport.ISelectionListenerWithAST; |
| import org.eclipse.dltk.ui.viewsupport.SelectionListenerWithASTManager; |
| import org.eclipse.dltk.utils.NatureExtensionManager; |
| import org.eclipse.jface.preference.IPreferenceStore; |
| import org.eclipse.jface.text.DocumentEvent; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IDocumentExtension4; |
| import org.eclipse.jface.text.IDocumentListener; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.ISelectionValidator; |
| import org.eclipse.jface.text.ISynchronizable; |
| import org.eclipse.jface.text.ITextInputListener; |
| import org.eclipse.jface.text.ITextSelection; |
| import org.eclipse.jface.text.ITextViewer; |
| import org.eclipse.jface.text.Position; |
| import org.eclipse.jface.text.link.LinkedModeModel; |
| import org.eclipse.jface.text.source.Annotation; |
| import org.eclipse.jface.text.source.IAnnotationModel; |
| import org.eclipse.jface.text.source.IAnnotationModelExtension; |
| import org.eclipse.jface.text.source.ISourceViewer; |
| import org.eclipse.jface.viewers.ISelection; |
| import org.eclipse.jface.viewers.ISelectionProvider; |
| import org.eclipse.swt.custom.StyledText; |
| import org.eclipse.ui.IEditorInput; |
| import org.eclipse.ui.IEditorSite; |
| import org.eclipse.ui.IWindowListener; |
| import org.eclipse.ui.IWorkbenchWindow; |
| import org.eclipse.ui.PlatformUI; |
| import org.eclipse.ui.texteditor.IDocumentProvider; |
| |
| /** |
| * @since 3.0 |
| */ |
| public class OccurrencesFinder { |
| /** |
| * Tells whether all occurrences of the element at the current caret |
| * location are automatically marked in this editor. |
| */ |
| private boolean fMarkOccurrenceAnnotations; |
| |
| /** |
| * Tells whether the occurrence annotations are sticky i.e. whether they |
| * stay even if there's no valid Java element at the current caret position. |
| * Only valid if {@link #fMarkOccurrenceAnnotations} is <code>true</code>. |
| */ |
| private boolean fStickyOccurrenceAnnotations; |
| |
| private OccurrencesFinderJob fOccurrencesFinderJob; |
| /** The occurrences finder job canceler */ |
| private OccurrencesFinderJobCanceler fOccurrencesFinderJobCanceler; |
| |
| /** |
| * The document modification stamp at the time when the last occurrence |
| * marking took place. |
| */ |
| private long fMarkOccurrenceModificationStamp = IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP; |
| |
| /** |
| * The region of the word under the caret used to when computing the current |
| * occurrence markings. |
| */ |
| private IRegion fMarkOccurrenceTargetRegion; |
| |
| /** |
| * The selection used when forcing occurrence marking through code. |
| */ |
| private ISelection fForcedMarkOccurrencesSelection; |
| |
| /** |
| * Holds the current occurrence annotations. |
| */ |
| private Annotation[] fOccurrenceAnnotations = null; |
| |
| private final ScriptEditor editor; |
| private final IOccurrencesFinder[] finders; |
| |
| public OccurrencesFinder(ScriptEditor editor) { |
| this.editor = editor; |
| final NatureExtensionManager<IOccurrencesFinder> occurrencesFinderManager = new NatureExtensionManager<>( |
| DLTKUIPlugin.PLUGIN_ID + ".search", IOccurrencesFinder.class); |
| finders = occurrencesFinderManager |
| .getInstances(editor.getLanguageToolkit().getNatureId()); |
| } |
| |
| /** |
| * Finds and marks occurrence annotations. |
| * |
| * @since 3.0 |
| */ |
| class OccurrencesFinderJob extends Job { |
| |
| private final IDocument fDocument; |
| private final ISelection fSelection; |
| private final ISelectionValidator fPostSelectionValidator; |
| private boolean fCanceled = false; |
| private final OccurrenceLocation[] fLocations; |
| |
| public OccurrencesFinderJob(IDocument document, |
| OccurrenceLocation[] locations, ISelection selection) { |
| super(DLTKEditorMessages.ScriptEditor_markOccurrences_job_name); |
| fDocument = document; |
| fSelection = selection; |
| fLocations = locations; |
| |
| if (getSelectionProvider() instanceof ISelectionValidator) |
| fPostSelectionValidator = (ISelectionValidator) getSelectionProvider(); |
| else |
| fPostSelectionValidator = null; |
| } |
| |
| // cannot use cancel() because it is declared final |
| void doCancel() { |
| fCanceled = true; |
| cancel(); |
| } |
| |
| private boolean isCanceled(IProgressMonitor progressMonitor) { |
| return fCanceled || progressMonitor.isCanceled() |
| || fPostSelectionValidator != null |
| && !(fPostSelectionValidator.isValid(fSelection) |
| || fForcedMarkOccurrencesSelection == fSelection) |
| || LinkedModeModel.hasInstalledModel(fDocument); |
| } |
| |
| /* |
| * @see Job#run(org.eclipse.core.runtime.IProgressMonitor) |
| */ |
| @Override |
| public IStatus run(IProgressMonitor progressMonitor) { |
| if (isCanceled(progressMonitor)) |
| return Status.CANCEL_STATUS; |
| |
| ITextViewer textViewer = getViewer(); |
| if (textViewer == null) |
| return Status.CANCEL_STATUS; |
| |
| IDocument document = textViewer.getDocument(); |
| if (document == null) |
| return Status.CANCEL_STATUS; |
| |
| IDocumentProvider documentProvider = getDocumentProvider(); |
| if (documentProvider == null) |
| return Status.CANCEL_STATUS; |
| |
| IAnnotationModel annotationModel = documentProvider |
| .getAnnotationModel(getEditorInput()); |
| if (annotationModel == null) |
| return Status.CANCEL_STATUS; |
| |
| // Add occurrence annotations |
| int length = fLocations.length; |
| Map<Annotation, Position> annotationMap = new HashMap<>(length); |
| for (int i = 0; i < length; i++) { |
| |
| if (isCanceled(progressMonitor)) |
| return Status.CANCEL_STATUS; |
| |
| OccurrenceLocation location = fLocations[i]; |
| Position position = new Position(location.getOffset(), |
| location.getLength()); |
| |
| String description = location.getDescription(); |
| String annotationType = "org.eclipse.dltk.ui.occurrences"; //$NON-NLS-1$ |
| |
| annotationMap.put( |
| new Annotation(annotationType, false, description), |
| position); |
| } |
| |
| if (isCanceled(progressMonitor)) |
| return Status.CANCEL_STATUS; |
| |
| synchronized (getLockObject(annotationModel)) { |
| if (annotationModel instanceof IAnnotationModelExtension) { |
| ((IAnnotationModelExtension) annotationModel) |
| .replaceAnnotations(fOccurrenceAnnotations, |
| annotationMap); |
| } else { |
| removeOccurrenceAnnotations(); |
| for (Map.Entry<Annotation, Position> mapEntry : annotationMap |
| .entrySet()) { |
| annotationModel.addAnnotation(mapEntry.getKey(), |
| mapEntry.getValue()); |
| } |
| } |
| fOccurrenceAnnotations = annotationMap.keySet() |
| .toArray(new Annotation[annotationMap.keySet().size()]); |
| } |
| |
| return Status.OK_STATUS; |
| } |
| } |
| |
| /** |
| * Cancels the occurrences finder job upon document changes. |
| * |
| * @since 3.0 |
| */ |
| class OccurrencesFinderJobCanceler |
| implements IDocumentListener, ITextInputListener { |
| |
| public void install() { |
| ISourceViewer sourceViewer = getViewer(); |
| if (sourceViewer == null) |
| return; |
| |
| StyledText text = sourceViewer.getTextWidget(); |
| if (text == null || text.isDisposed()) |
| return; |
| |
| sourceViewer.addTextInputListener(this); |
| |
| IDocument document = sourceViewer.getDocument(); |
| if (document != null) |
| document.addDocumentListener(this); |
| } |
| |
| public void uninstall() { |
| ISourceViewer sourceViewer = getViewer(); |
| if (sourceViewer != null) |
| sourceViewer.removeTextInputListener(this); |
| |
| IDocumentProvider documentProvider = getDocumentProvider(); |
| if (documentProvider != null) { |
| IDocument document = documentProvider |
| .getDocument(getEditorInput()); |
| if (document != null) |
| document.removeDocumentListener(this); |
| } |
| } |
| |
| /* |
| * @see |
| * org.eclipse.jface.text.IDocumentListener#documentAboutToBeChanged(org |
| * .eclipse.jface.text.DocumentEvent) |
| */ |
| @Override |
| public void documentAboutToBeChanged(DocumentEvent event) { |
| if (fOccurrencesFinderJob != null) |
| fOccurrencesFinderJob.doCancel(); |
| } |
| |
| /* |
| * @see |
| * org.eclipse.jface.text.IDocumentListener#documentChanged(org.eclipse. |
| * jface.text.DocumentEvent) |
| */ |
| @Override |
| public void documentChanged(DocumentEvent event) { |
| } |
| |
| /* |
| * @see org.eclipse.jface.text.ITextInputListener# |
| * inputDocumentAboutToBeChanged (org.eclipse.jface.text.IDocument, |
| * org.eclipse.jface.text.IDocument) |
| */ |
| @Override |
| public void inputDocumentAboutToBeChanged(IDocument oldInput, |
| IDocument newInput) { |
| if (oldInput == null) |
| return; |
| |
| oldInput.removeDocumentListener(this); |
| } |
| |
| /* |
| * @see |
| * org.eclipse.jface.text.ITextInputListener#inputDocumentChanged(org |
| * .eclipse .jface.text.IDocument, org.eclipse.jface.text.IDocument) |
| */ |
| @Override |
| public void inputDocumentChanged(IDocument oldInput, |
| IDocument newInput) { |
| if (newInput == null) |
| return; |
| newInput.addDocumentListener(this); |
| } |
| } |
| |
| private ActivationListener fActivationListener = new ActivationListener(); |
| private ISelectionListenerWithAST fPostSelectionListenerWithAST; |
| |
| /** |
| * Internal activation listener. |
| * |
| * @since 3.0 |
| */ |
| private class ActivationListener implements IWindowListener { |
| |
| /* |
| * @see org.eclipse.ui.IWindowListener#windowActivated(org.eclipse.ui. |
| * IWorkbenchWindow) |
| * |
| * @since 3.1 |
| */ |
| @Override |
| public void windowActivated(IWorkbenchWindow window) { |
| if (window == getEditorSite().getWorkbenchWindow() |
| && fMarkOccurrenceAnnotations && isActivePart()) { |
| fForcedMarkOccurrencesSelection = getSelectionProvider() |
| .getSelection(); |
| ISourceModule inputElement = getInputElement(); |
| if (inputElement != null) |
| updateOccurrenceAnnotations( |
| (ITextSelection) fForcedMarkOccurrencesSelection, |
| inputElement, |
| getAST(inputElement, getProgressMonitor())); |
| } |
| } |
| |
| /* |
| * @see org.eclipse.ui.IWindowListener#windowDeactivated(org.eclipse.ui. |
| * IWorkbenchWindow) |
| * |
| * @since 3.1 |
| */ |
| @Override |
| public void windowDeactivated(IWorkbenchWindow window) { |
| if (window == getEditorSite().getWorkbenchWindow() |
| && fMarkOccurrenceAnnotations && isActivePart()) |
| removeOccurrenceAnnotations(); |
| } |
| |
| /* |
| * @see org.eclipse.ui.IWindowListener#windowClosed(org.eclipse.ui. |
| * IWorkbenchWindow) |
| * |
| * @since 3.1 |
| */ |
| @Override |
| public void windowClosed(IWorkbenchWindow window) { |
| } |
| |
| /* |
| * @see org.eclipse.ui.IWindowListener#windowOpened(org.eclipse.ui. |
| * IWorkbenchWindow) |
| * |
| * @since 3.1 |
| */ |
| @Override |
| public void windowOpened(IWorkbenchWindow window) { |
| } |
| } |
| |
| public void install() { |
| PlatformUI.getWorkbench().addWindowListener(fActivationListener); |
| if (isMarkingOccurrences()) { |
| installOccurrencesFinder(false); |
| } |
| } |
| |
| public void dispose() { |
| // cancel possible running computation |
| fMarkOccurrenceAnnotations = false; |
| uninstallOccurrencesFinder(); |
| |
| if (fActivationListener != null) { |
| PlatformUI.getWorkbench().removeWindowListener(fActivationListener); |
| fActivationListener = null; |
| } |
| } |
| |
| private IPreferenceStore preferenceStore; |
| |
| public void setPreferenceStore(IPreferenceStore store) { |
| preferenceStore = store; |
| fMarkOccurrenceAnnotations = store |
| .getBoolean(PreferenceConstants.EDITOR_MARK_OCCURRENCES); |
| fStickyOccurrenceAnnotations = store |
| .getBoolean(PreferenceConstants.EDITOR_STICKY_OCCURRENCES); |
| } |
| |
| /** |
| * Checks if "mark occurrences" is enabled in preferences |
| * |
| * @return |
| */ |
| public boolean isMarkingOccurrences() { |
| return preferenceStore != null && preferenceStore |
| .getBoolean(PreferenceConstants.EDITOR_MARK_OCCURRENCES); |
| } |
| |
| protected boolean handlePreferenceStoreChanged(String property, |
| boolean newValue) { |
| if (PreferenceConstants.EDITOR_MARK_OCCURRENCES.equals(property)) { |
| if (newValue != fMarkOccurrenceAnnotations) { |
| fMarkOccurrenceAnnotations = newValue; |
| if (!newValue) |
| uninstallOccurrencesFinder(); |
| else |
| installOccurrencesFinder(true); |
| } |
| return true; |
| } else if (PreferenceConstants.EDITOR_STICKY_OCCURRENCES |
| .equals(property)) { |
| fStickyOccurrenceAnnotations = newValue; |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| protected void installOccurrencesFinder(boolean forceUpdate) { |
| fMarkOccurrenceAnnotations = true; |
| |
| fPostSelectionListenerWithAST = (part, selection, module, |
| astRoot) -> updateOccurrenceAnnotations(selection, module, |
| astRoot); |
| SelectionListenerWithASTManager.getDefault().addListener(editor, |
| fPostSelectionListenerWithAST); |
| if (forceUpdate && getSelectionProvider() != null) { |
| fForcedMarkOccurrencesSelection = getSelectionProvider() |
| .getSelection(); |
| ISourceModule inputElement = getInputElement(); |
| if (inputElement != null) |
| updateOccurrenceAnnotations( |
| (ITextSelection) fForcedMarkOccurrencesSelection, |
| inputElement, |
| getAST(inputElement, getProgressMonitor())); |
| } |
| |
| if (fOccurrencesFinderJobCanceler == null) { |
| fOccurrencesFinderJobCanceler = new OccurrencesFinderJobCanceler(); |
| fOccurrencesFinderJobCanceler.install(); |
| } |
| } |
| |
| protected void uninstallOccurrencesFinder() { |
| fMarkOccurrenceAnnotations = false; |
| |
| if (fOccurrencesFinderJob != null) { |
| fOccurrencesFinderJob.cancel(); |
| fOccurrencesFinderJob = null; |
| } |
| |
| if (fOccurrencesFinderJobCanceler != null) { |
| fOccurrencesFinderJobCanceler.uninstall(); |
| fOccurrencesFinderJobCanceler = null; |
| } |
| |
| if (fPostSelectionListenerWithAST != null) { |
| SelectionListenerWithASTManager.getDefault().removeListener(editor, |
| fPostSelectionListenerWithAST); |
| fPostSelectionListenerWithAST = null; |
| } |
| |
| removeOccurrenceAnnotations(); |
| } |
| |
| public void updateOccurrenceAnnotations() { |
| ISelectionProvider selectionProvider = getSelectionProvider(); |
| if (selectionProvider == null) |
| return; |
| |
| ISelection textSelection = selectionProvider.getSelection(); |
| if (!(textSelection instanceof ITextSelection)) |
| return; |
| |
| ISourceModule inputElement = getInputElement(); |
| if (inputElement == null) |
| return; |
| |
| IModuleDeclaration ast = getAST(inputElement, getProgressMonitor()); |
| if (ast != null) { |
| fForcedMarkOccurrencesSelection = textSelection; |
| updateOccurrenceAnnotations((ITextSelection) textSelection, |
| inputElement, ast); |
| } |
| } |
| |
| /** |
| * Updates the occurrences annotations based on the current selection. |
| * |
| * @param selection |
| * the text selection |
| * @param astRoot |
| * the compilation unit AST |
| * @since 3.0 |
| */ |
| protected void updateOccurrenceAnnotations(ITextSelection selection, |
| ISourceModule module, IModuleDeclaration astRoot) { |
| |
| if (fOccurrencesFinderJob != null) |
| fOccurrencesFinderJob.cancel(); |
| |
| if (!fMarkOccurrenceAnnotations) |
| return; |
| |
| if (astRoot == null || selection == null) |
| return; |
| |
| IDocument document = getViewer().getDocument(); |
| if (document == null) |
| return; |
| |
| boolean hasChanged = false; |
| if (document instanceof IDocumentExtension4) { |
| int offset = selection.getOffset(); |
| long currentModificationStamp = ((IDocumentExtension4) document) |
| .getModificationStamp(); |
| IRegion markOccurrenceTargetRegion = fMarkOccurrenceTargetRegion; |
| hasChanged = currentModificationStamp != fMarkOccurrenceModificationStamp; |
| if (markOccurrenceTargetRegion != null && !hasChanged) { |
| if (markOccurrenceTargetRegion.getOffset() <= offset |
| && offset <= markOccurrenceTargetRegion.getOffset() |
| + markOccurrenceTargetRegion.getLength()) |
| return; |
| } |
| fMarkOccurrenceTargetRegion = ScriptWordFinder.findWord(document, |
| offset); |
| fMarkOccurrenceModificationStamp = currentModificationStamp; |
| } |
| |
| OccurrenceLocation[] locations = null; |
| |
| if (finders != null) { |
| for (IOccurrencesFinder finder : finders) { |
| if (finder.initialize(module, astRoot, selection.getOffset(), |
| selection.getLength()) == null) { |
| locations = finder.getOccurrences(); |
| if (locations != null) { |
| break; |
| } |
| } |
| } |
| } |
| |
| if (locations == null) { |
| if (!fStickyOccurrenceAnnotations) |
| removeOccurrenceAnnotations(); |
| else if (hasChanged) // check consistency of current annotations |
| removeOccurrenceAnnotations(); |
| return; |
| } |
| |
| fOccurrencesFinderJob = new OccurrencesFinderJob(document, locations, |
| selection); |
| // fOccurrencesFinderJob.setPriority(Job.DECORATE); |
| // fOccurrencesFinderJob.setSystem(true); |
| // fOccurrencesFinderJob.schedule(); |
| fOccurrencesFinderJob.run(new NullProgressMonitor()); |
| } |
| |
| void removeOccurrenceAnnotations() { |
| fMarkOccurrenceModificationStamp = IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP; |
| fMarkOccurrenceTargetRegion = null; |
| |
| IDocumentProvider documentProvider = getDocumentProvider(); |
| if (documentProvider == null) |
| return; |
| |
| IAnnotationModel annotationModel = documentProvider |
| .getAnnotationModel(getEditorInput()); |
| if (annotationModel == null || fOccurrenceAnnotations == null) |
| return; |
| |
| synchronized (getLockObject(annotationModel)) { |
| if (annotationModel instanceof IAnnotationModelExtension) { |
| ((IAnnotationModelExtension) annotationModel) |
| .replaceAnnotations(fOccurrenceAnnotations, null); |
| } else { |
| for (int i = 0, length = fOccurrenceAnnotations.length; i < length; i++) |
| annotationModel.removeAnnotation(fOccurrenceAnnotations[i]); |
| } |
| fOccurrenceAnnotations = null; |
| } |
| } |
| |
| /** |
| * Returns the lock object for the given annotation model. |
| * |
| * @param annotationModel |
| * the annotation model |
| * @return the annotation model's lock object |
| * @since 3.0 |
| */ |
| private static Object getLockObject(IAnnotationModel annotationModel) { |
| if (annotationModel instanceof ISynchronizable) { |
| Object lock = ((ISynchronizable) annotationModel).getLockObject(); |
| if (lock != null) |
| return lock; |
| } |
| return annotationModel; |
| } |
| |
| // /////////////// |
| |
| protected boolean isActivePart() { |
| return editor.isActivePart(); |
| } |
| |
| protected IEditorSite getEditorSite() { |
| return editor.getEditorSite(); |
| } |
| |
| protected IModuleDeclaration getAST(IModelElement inputElement, |
| IProgressMonitor progressMonitor) { |
| return SourceParserUtil.parse((ISourceModule) inputElement, null); |
| } |
| |
| protected ISourceModule getInputElement() { |
| return (ISourceModule) editor.getInputModelElement(); |
| } |
| |
| protected IProgressMonitor getProgressMonitor() { |
| return editor.getProgressMonitor(); |
| } |
| |
| protected ISelectionProvider getSelectionProvider() { |
| return editor.getSelectionProvider(); |
| } |
| |
| protected IDocumentProvider getDocumentProvider() { |
| return editor.getDocumentProvider(); |
| } |
| |
| protected IEditorInput getEditorInput() { |
| return editor.getEditorInput(); |
| } |
| |
| protected ISourceViewer getViewer() { |
| return editor.getViewer(); |
| } |
| |
| /** |
| * Checks if this object is correctly configured, i.e. has necessary finders |
| * installed, etc. |
| * |
| * @return |
| */ |
| public boolean isValid() { |
| return finders != null; |
| } |
| |
| } |