/*******************************************************************************
* Copyright (c) 2007 IBM Corporation.
* 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:
* Robert Fuhrer (rfuhrer@watson.ibm.com) - initial API and implementation
* - refinement and hardening
* Stan Sutton (suttons@us.ibm.com) - refinement and hardening
*******************************************************************************/
package org.eclipse.imp.editor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.imp.core.ErrorHandler;
import org.eclipse.imp.parser.IParseController;
import org.eclipse.imp.parser.ISourcePositionLocator;
import org.eclipse.imp.runtime.RuntimePlugin;
import org.eclipse.imp.services.IOccurrenceMarker;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.ISynchronizable;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.Position;
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.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.IWorkbenchWindowActionDelegate;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;
/**
* Action class that implements the "Mark Occurrences" mode. This action contains a number of
* nested listener classes that monitor which editor is active, document changes, and selection
* changes, and computes a set of "occurrence" annotations in the editor, using the language-specific
* "mark occurrences" service.
*/
public class MarkOccurrencesAction implements IWorkbenchWindowActionDelegate {
/**
* The ID for the kind of annotations created for "mark occurrences"
*/
public static final String OCCURRENCE_ANNOTATION= RuntimePlugin.IMP_RUNTIME + ".occurrenceAnnotation";
/**
* True if "mark occurrences" is currently on/enabled
*/
private boolean fMarkingEnabled = false;
private ITextEditor fActiveEditor;
/**
* The IParseController for the currently-active editor, if any. Could be null
* if the current editor is not an IMP editor.
*/
private IParseController fParseController;
/**
* The document provider for the currently-active editor. Could be null if
* the current editor is not an IMP editor.
*/
private IDocumentProvider fDocumentProvider;
/**
* The document for the currently-active editor, if any. Could be null if
* the current editor is not an IMP editor.
*/
private IDocument fDocument;
/**
* The AST for the currently-active editor.
*/
private Object fCompilationUnit;
/**
* The language-specific "mark occurrences" service implementation, if any.
*/
private IOccurrenceMarker fOccurrenceMarker;
private Annotation[] fOccurrenceAnnotations;
private ISelectionChangedListener fSelectionListener;
private IDocumentListener fDocumentListener;
private IPartListener fPartListener;
/**
* Listens to part-related events from the workbench to monitor when text editors are
* activated/closed, and keep the necessary listeners pointed at the active editor.
*/
private final class EditorPartListener implements IPartListener {
public void partActivated(IWorkbenchPart part) {
if (part instanceof ITextEditor && fMarkingEnabled) {
setUpActiveEditor((ITextEditor) part);
if (fDocumentProvider == null)
return;
IAnnotationModel annotationModel= fDocumentProvider.getAnnotationModel(getEditorInput());
// Need to initialize the set of pre-existing annotations in order
// for them to be removed properly when new occurrences are marked
if (annotationModel != null) {
@SuppressWarnings("unchecked")
Iterator<Annotation> annotationIterator = annotationModel.getAnnotationIterator();
List<Annotation> annotationList = new ArrayList<Annotation>();
while (annotationIterator.hasNext()) {
// SMS 23 Jul 2008: added test for annotation type
Annotation ann = (Annotation) annotationIterator.next();
if (ann.getType().indexOf(OCCURRENCE_ANNOTATION) > -1) {
annotationList.add(ann);
}
}
fOccurrenceAnnotations = annotationList.toArray(new Annotation[annotationList.size()]);
}
}
if (!fMarkingEnabled) {
unregisterListeners();
removeExistingOccurrenceAnnotations();
}
}
public void partClosed(IWorkbenchPart part) {
if (part == fActiveEditor) {
unregisterListeners();
fActiveEditor= null;
fCompilationUnit= null;
fDocumentProvider= null;
fDocument= null;
fParseController= null;
fOccurrenceMarker= null;
fOccurrenceAnnotations= null;
}
}
public void partBroughtToTop(IWorkbenchPart part) { }
public void partDeactivated(IWorkbenchPart part) { }
public void partOpened(IWorkbenchPart part) { }
}
/**
* Listens to document changes and invalidates the AST cache to force a re-parsing.
*/
private final class DocumentListener implements IDocumentListener {
public void documentAboutToBeChanged(DocumentEvent event) { }
public void documentChanged(DocumentEvent event) {
fCompilationUnit= null;
}
}
/**
* Listens to selection changes and forces a recomputation of the annotations.
* The analysis is performed once each time the compilation unit in the active
* editor changes, and the results are reused each time the selection changes
* in order to produce the annotations corresponding to the selection.
*/
private final class SelectionListener implements ISelectionChangedListener {
private ISelection previousSelection = null;
private SelectionListener() { }
public void selectionChanged(SelectionChangedEvent event) {
ISelection selection= event.getSelection();
if (selection instanceof ITextSelection) {
if (previousSelection != null && previousSelection.equals(selection)) {
return;
}
previousSelection = selection;
ITextSelection textSel= (ITextSelection) selection;
int offset= textSel.getOffset();
int length= textSel.getLength();
if (length == 0)
return;
recomputeAnnotationsForSelection(offset, length, fDocument);
}
}
}
/*
* Notes on the interaction of text folding and occurrence marking:
*
* When you click on a text-folding widget, you can generate two events: one signaling a
* change to the project model, the other signaling a change to the text selection (even
* though you have not directly clicked in text). The selection-changed event is propagated
* to the listener posted by MarkOccurrencesAction, just as selection-changed events that
* actually correspond to changes of selections within the text. Only selection-changed
* events originating within the text are of interest here. More to the point, selection-
* changed events from the folding widgets should be ignored because they may cause the
* selection (and dependent markings) to be changed in unanticipated and undesirable ways.
* In general, we do not expect the text selection and dependent markings to change just
* because the text has been folded (or unfolded).
*
* Unfortunately, while we can listen for updates to the projection annotation model,
* there appears to be no definite way to correlate changes in that model to changes in
* the annotation model that contains the selection annotations. The projection annotation
* events seem to be generated after the corresponding selection-changed events, and they
* lack a text position or timestamp that would allow them to be correlated with the
* selection-changed events.
*
* The one potentially useful characteristic of the selection-changed events that come
* from changes to the projection annotation model is that they seem to have a length of 0.
* On that basis, the selection listener implemented below filters out events of 0 length.
* This approach assumes that such events can be ignored. In practice this does prevent
* changes to the projection annotation model from causing inappropriate updates to the
* text selection. Some genuine selections of length 0 may be missed by this approach,
* but perhaps most text selections of interest will have length greater than 0. If not,
* then we can revisit this issue.
*/
public MarkOccurrencesAction() { }
public void run(IAction action) {
fMarkingEnabled = action.isChecked();
if (fMarkingEnabled) {
setUpActiveEditor((ITextEditor) PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getActiveEditor());
} else {
unregisterListeners();
removeExistingOccurrenceAnnotations();
}
}
private void registerListeners() {
// getDocumentFromEditor() can return null, but register listeners
// should only be called when there is an active editor that can
// be presumed to have a document provider that has document
IDocument document = getDocumentFromEditor();
if (document == null)
return;
fSelectionListener= new SelectionListener();
fDocumentListener= new DocumentListener();
fActiveEditor.getSelectionProvider().addSelectionChangedListener(fSelectionListener);
document.addDocumentListener(fDocumentListener);
}
private void unregisterListeners() {
if (fActiveEditor == null)
return;
if (fSelectionListener != null) {
ISelectionProvider provider = fActiveEditor.getSelectionProvider();
if (provider != null)
fActiveEditor.getSelectionProvider().removeSelectionChangedListener(fSelectionListener);
}
if (fDocumentListener != null) {
IDocument document = getDocumentFromEditor();
if (document != null)
document.removeDocumentListener(fDocumentListener);
}
if (fPartListener != null) {
fActiveEditor.getSite().getPage().removePartListener(fPartListener);
}
}
private IDocument getDocumentFromEditor() {
IDocumentProvider provider = getDocumentProvider();
if (provider != null)
return provider.getDocument(getEditorInput());
else
return null;
}
private void recomputeAnnotationsForSelection(int offset, int length, IDocument document) {
IAnnotationModel annotationModel= fDocumentProvider.getAnnotationModel(getEditorInput());
Object root= getCompilationUnit();
if (root == null) {
// Get this when "selecting" an error message that is shown in the editor view
// but is not part of the source file; just returning should leave previous
// markings, if any, as they were (which is probably fine)
// Also get this when the current AST is null, e.g., as in the event of
// a parse error
// System.err.println("MarkOccurrencesAction.recomputeAnnotationsForSelection(..): root of current AST is null; returning");
return;
}
Object selectedNode= fParseController.getSourcePositionLocator().findNode(root, offset, offset+length-1);
if (fOccurrenceMarker == null) {
// It might be possible to set the active editor at this point under
// some circumstances, but attempting to do so under other circumstances
// can lead to stack overflow, so just return.
return;
}
try {
List<Object> occurrences= fOccurrenceMarker.getOccurrencesOf(fParseController, selectedNode);
if (occurrences != null) {
Position[] positions= convertRefNodesToPositions(occurrences);
placeAnnotations(convertPositionsToAnnotationMap(positions, document), annotationModel);
} else {
ErrorHandler.reportError("Occurrence marker returned a null list of occurrences.");
}
} catch (Exception e) {
ErrorHandler.reportError("Error obtaining occurrences of selected node", e);
}
}
private Map<Annotation, Position> convertPositionsToAnnotationMap(Position[] positions, IDocument document) {
Map<Annotation, Position> annotationMap= new HashMap<Annotation, Position>(positions.length);
for(int i= 0; i < positions.length; i++) {
Position position= positions[i];
try { // Create & add annotation
String message= document.get(position.offset, position.length);
annotationMap.put(new Annotation(OCCURRENCE_ANNOTATION, false, message), position);
} catch (BadLocationException ex) {
continue; // skip apparently bogus position
}
}
return annotationMap;
}
private void placeAnnotations(Map<Annotation,Position> annotationMap, IAnnotationModel annotationModel) {
Object lockObject= getLockObject(annotationModel);
synchronized (lockObject) {
if (annotationModel instanceof IAnnotationModelExtension) {
((IAnnotationModelExtension) annotationModel).replaceAnnotations(fOccurrenceAnnotations, annotationMap);
} else {
removeExistingOccurrenceAnnotations();
Iterator<Map.Entry<Annotation,Position>> iter= annotationMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<Annotation,Position> mapEntry= iter.next();
annotationModel.addAnnotation((Annotation) mapEntry.getKey(), (Position) mapEntry.getValue());
}
}
fOccurrenceAnnotations= (Annotation[]) annotationMap.keySet().toArray(new Annotation[annotationMap.keySet().size()]);
}
}
void removeExistingOccurrenceAnnotations() {
// RMF 6/27/2008 - If we've come up in an empty workspace, there won't be an active editor
if (fActiveEditor == null)
return;
// RMF 6/27/2008 - Apparently partActivated() gets called before the editor is initialized
// (on MacOS?), and then we can't properly initialize this MarkOccurrencesAction instance.
// When that happens, fDocumentProvider will be null. Initialization needs a fix for that,
// rather than this simple-minded null guard.
if (fDocumentProvider == null)
return;
IAnnotationModel annotationModel= fDocumentProvider.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;
}
}
private Position[] convertRefNodesToPositions(List<Object> refs) {
Position[] positions= new Position[refs.size()];
int i= 0;
ISourcePositionLocator locator= fParseController.getSourcePositionLocator();
for(Iterator<Object> iter= refs.iterator(); iter.hasNext(); i++) {
Object node= iter.next();
positions[i]= new Position(locator.getStartOffset(node), locator.getLength(node)+1);
}
return positions;
}
private Object getCompilationUnit() {
// Do NOT compute fCompilationUnit conditionally based
// on the AST being null; that causes problems when switching
// between editor windows because the old value of the AST
// will be retained even after the new window comes up, until
// the text in the new window is parsed. For now just
// get the current AST (but in the future do something more
// sophisticated to avoid needless recomputation but only
// when it is truly needless).
fCompilationUnit= fParseController.getCurrentAst();
return fCompilationUnit;
}
private IEditorInput getEditorInput() {
return fActiveEditor.getEditorInput();
}
private IDocumentProvider getDocumentProvider() {
fDocumentProvider= fActiveEditor.getDocumentProvider();
return fDocumentProvider;
}
private void setUpActiveEditor(ITextEditor textEditor) {
unregisterListeners();
if (textEditor == null)
return;
fActiveEditor = textEditor;
LanguageServiceManager fLanguageServiceManager = LanguageServiceManager.getMyServiceManager(fActiveEditor);
if (fLanguageServiceManager == null) {
// Should disable/hide this action - but how?
return;
} else {
// Enable/make visible - but how?
}
fDocument= getDocumentFromEditor();
fParseController = fLanguageServiceManager.getParseController();
if (fParseController == null) {
return;
}
fOccurrenceMarker = fLanguageServiceManager.getOccurrenceMarker();
registerListeners();
ISelection selection = fActiveEditor.getSelectionProvider().getSelection();
if (selection instanceof ITextSelection) {
ITextSelection textSelection = (ITextSelection) selection;
recomputeAnnotationsForSelection(textSelection.getOffset(), textSelection.getLength(), fDocument);
}
}
private Object getLockObject(IAnnotationModel annotationModel) {
if (annotationModel instanceof ISynchronizable)
return ((ISynchronizable) annotationModel).getLockObject();
else
return annotationModel;
}
public void selectionChanged(IAction action, ISelection selection) { }
public void dispose() {
unregisterListeners();
}
public void init(IWorkbenchWindow window) {
window.getActivePage().addPartListener(new EditorPartListener());
}
}