package com.redhat.ceylon.eclipse.code.editor;
import static com.redhat.ceylon.eclipse.ui.CeylonPlugin.PLUGIN_ID;
import static com.redhat.ceylon.eclipse.util.EditorUtil.getActivePage;
import static com.redhat.ceylon.eclipse.util.Nodes.findNode;
import static com.redhat.ceylon.eclipse.util.Nodes.getIdentifyingNode;
import static com.redhat.ceylon.eclipse.util.Nodes.getReferencedExplicitDeclaration;
import static java.util.Collections.emptyList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.antlr.runtime.CommonToken;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
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.SelectionChangedEvent;
import org.eclipse.swt.custom.CaretEvent;
import org.eclipse.swt.custom.CaretListener;
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.texteditor.IDocumentProvider;
import com.redhat.ceylon.compiler.typechecker.tree.Node;
import com.redhat.ceylon.compiler.typechecker.tree.Tree;
import com.redhat.ceylon.eclipse.code.hover.DocumentationView;
import com.redhat.ceylon.eclipse.code.parse.CeylonParseController;
import com.redhat.ceylon.eclipse.code.parse.TreeLifecycleListener;
import com.redhat.ceylon.ide.common.util.FindAssignmentsVisitor;
import com.redhat.ceylon.ide.common.util.FindDeclarationNodeVisitor;
import com.redhat.ceylon.ide.common.util.FindReferencesVisitor;
import com.redhat.ceylon.model.typechecker.model.Declaration;
import com.redhat.ceylon.model.typechecker.model.Referenceable;
public class MarkOccurrencesAction
implements IWorkbenchWindowActionDelegate,
CaretListener,
ISelectionChangedListener,
TreeLifecycleListener {
private static final Annotation[] NO_ANNOTATIONS = new Annotation[0];
/**
* The ID for the kind of annotations created for "mark occurrences"
*/
public static final String DECLARATION_ANNOTATION =
PLUGIN_ID + ".declarationAnnotation";
public static final String OCCURRENCE_ANNOTATION =
PLUGIN_ID + ".occurrenceAnnotation";
public static final String ASSIGNMENT_ANNOTATION =
PLUGIN_ID + ".assignmentAnnotation";
/**
* True if "mark occurrences" is currently on/enabled
*/
private boolean markingEnabled = true;
private CeylonEditor activeEditor;
/**
* The IParseController for the currently-active editor, if any. Could be null
* if the current editor is not an IMP editor.
*/
private CeylonParseController parseController;
/**
* The document provider for the currently-active editor. Could be null if
* the current editor is not an IMP editor.
*/
private IDocumentProvider documentProvider;
/**
* The document for the currently-active editor, if any. Could be null if
* the current editor is not an IMP editor.
*/
private IDocument document;
private Annotation[] occurrenceAnnotations;
/**
* 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 {
@Override
public void partActivated(IWorkbenchPart part) {
if (part instanceof CeylonEditor) {
setUpActiveEditor((CeylonEditor) part);
if (documentProvider!=null) {
retrieveOccurrenceAnnotations();
if (!markingEnabled) {
unregisterListeners();
removeExistingOccurrenceAnnotations();
}
}
}
}
@Override
public void partClosed(IWorkbenchPart part) {
if (part == activeEditor) {
unregisterListeners();
activeEditor = null;
documentProvider = null;
document = null;
parseController = null;
occurrenceAnnotations = null;
DocumentationView documentationView =
DocumentationView.getInstance();
if (documentationView!=null) {
documentationView.update(null, -1, -1);
}
}
}
@Override
public void partBroughtToTop(IWorkbenchPart part) {}
@Override
public void partDeactivated(IWorkbenchPart part) {}
@Override
public void partOpened(IWorkbenchPart part) {}
}
private boolean canRecompute() {
return !activeEditor.isBackgroundParsingPaused() &&
!activeEditor.isBlockSelectionModeEnabled() &&
!activeEditor.isInLinkedMode();
}
@Override
public void caretMoved(CaretEvent event) {
try {
if (canRecompute()) {
int offset =
activeEditor.getCeylonSourceViewer()
.widgetOffset2ModelOffset(
event.caretOffset);
recomputeAnnotationsForSelection(offset, 0,
document);
}
}
catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void selectionChanged(SelectionChangedEvent event) {
try {
ISelection sel = event.getSelection();
if (sel instanceof ITextSelection &&
canRecompute()) {
ITextSelection selection =
(ITextSelection) sel;
recomputeAnnotationsForSelection(
selection.getOffset(),
selection.getLength(),
document);
}
}
catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run(IAction action) {
markingEnabled = action.isChecked();
if (markingEnabled) {
CeylonEditor editor =
(CeylonEditor)
getActivePage()
.getActiveEditor();
setUpActiveEditor(editor);
}
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) {
CeylonSourceViewer sourceViewer =
activeEditor.getCeylonSourceViewer();
if (sourceViewer!=null) {
sourceViewer.getTextWidget()
.addCaretListener(this);
sourceViewer.getSelectionProvider()
.addSelectionChangedListener(this);
}
}
activeEditor.addModelListener(this);
}
private void unregisterListeners() {
if (activeEditor!=null) {
CeylonSourceViewer sourceViewer =
activeEditor.getCeylonSourceViewer();
if (sourceViewer!=null) {
sourceViewer.getTextWidget()
.removeCaretListener(this);
sourceViewer.getSelectionProvider()
.removeSelectionChangedListener(this);
}
activeEditor.removeModelListener(this);
}
}
private IDocument getDocumentFromEditor() {
IDocumentProvider provider = getDocumentProvider();
return provider==null ? null :
provider.getDocument(getEditorInput());
}
private void recomputeAnnotationsForSelection(
int offset, int length,
IDocument document) {
IAnnotationModel annotationModel =
documentProvider.getAnnotationModel(
getEditorInput());
Tree.CompilationUnit 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
return;
}
List<CommonToken> tokens =
activeEditor==null ? null :
activeEditor.getParseController()
.getTokens();
Node selectedNode =
findNode(root, tokens,
offset, offset+length);
try {
List<Node> declarations =
getDeclarationsOf(parseController,
selectedNode);
List<Node> occurrences =
getOccurrencesOf(parseController,
selectedNode);
List<Node> assignments =
getAssignmentsOf(parseController,
selectedNode);
// removeEditedOccurrences(document, declarations);
// removeEditedOccurrences(document, occurrences);
// removeEditedOccurrences(document, assignments);
int capacity =
declarations.size() +
occurrences.size() +
assignments.size();
Map<Annotation,Position> annotationMap =
new HashMap<Annotation,Position>
(capacity);
addPositionsToAnnotationMap(
convertRefNodesToPositions(declarations),
DECLARATION_ANNOTATION, document,
annotationMap);
addPositionsToAnnotationMap(
convertRefNodesToPositions(occurrences),
OCCURRENCE_ANNOTATION, document,
annotationMap);
addPositionsToAnnotationMap(
convertRefNodesToPositions(assignments),
ASSIGNMENT_ANNOTATION, document,
annotationMap);
placeAnnotations(annotationMap, annotationModel);
}
catch (Exception e) {
e.printStackTrace();
}
DocumentationView documentationView =
DocumentationView.getInstance();
if (documentationView!=null) {
documentationView.update(activeEditor,
offset, length);
}
}
/*private static void removeEditedOccurrences(
IDocument document,
List<Node> occurrences)
throws BadLocationException {
for (Iterator<Node> it=occurrences.iterator();
it.hasNext();) {
Node next = it.next();
if (next != null) {
CommonToken tok =
(CommonToken)
next.getToken();
if (tok != null) {
try {
int start = tok.getStartIndex();
int stop = tok.getStopIndex();
int len = stop-start+1;
String docText =
document.get(start, len);
if (docText.startsWith("\\")) {
docText = docText.substring(2);
}
if (!docText.equals(tok.getText())) {
it.remove();
}
}
catch (BadLocationException e) {
it.remove();
}
}
}
}
}*/
private void addPositionsToAnnotationMap(
Position[] positions, String type,
IDocument document,
Map<Annotation, Position> annotationMap) {
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(type, false, message),
position);
}
catch (BadLocationException ex) {
continue; // skip apparently bogus position
}
}
}
private void placeAnnotations(
Map<Annotation,Position> annotationMap,
IAnnotationModel annotationModel) {
if (annotationModel==null ||
(occurrenceAnnotations==null ||
occurrenceAnnotations.length==0) &&
annotationMap.isEmpty()) {
return;
}
synchronized (getLockObject(annotationModel)) {
if (annotationModel
instanceof IAnnotationModelExtension) {
IAnnotationModelExtension ext =
(IAnnotationModelExtension)
annotationModel;
ext.replaceAnnotations(occurrenceAnnotations,
annotationMap);
}
else {
removeExistingOccurrenceAnnotations();
Iterator<Map.Entry<Annotation,Position>> iter =
annotationMap.entrySet()
.iterator();
while (iter.hasNext()) {
Map.Entry<Annotation,Position> mapEntry =
iter.next();
Annotation ann = mapEntry.getKey();
Position pos = mapEntry.getValue();
annotationModel.addAnnotation(ann, pos);
}
}
occurrenceAnnotations =
(Annotation[])
annotationMap.keySet()
.toArray(NO_ANNOTATIONS);
}
}
private void retrieveOccurrenceAnnotations() {
IAnnotationModel annotationModel =
documentProvider.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 = annotationIterator.next();
String type = ann.getType();
if (type.startsWith(DECLARATION_ANNOTATION) ||
type.startsWith(OCCURRENCE_ANNOTATION) ||
type.startsWith(ASSIGNMENT_ANNOTATION)) {
annotationList.add(ann);
}
}
occurrenceAnnotations =
annotationList.toArray(NO_ANNOTATIONS);
}
}
void removeExistingOccurrenceAnnotations() {
// RMF 6/27/2008 - If we've come up in an empty workspace, there won't be an active editor
if (activeEditor == 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 (documentProvider == null)
return;
IAnnotationModel annotationModel =
documentProvider.getAnnotationModel(
getEditorInput());
if (annotationModel==null ||
occurrenceAnnotations==null)
return;
synchronized (getLockObject(annotationModel)) {
if (annotationModel
instanceof IAnnotationModelExtension) {
IAnnotationModelExtension ext =
(IAnnotationModelExtension)
annotationModel;
ext.replaceAnnotations(occurrenceAnnotations, null);
}
else {
for (int i=0,
length=occurrenceAnnotations.length;
i<length;
i++) {
annotationModel.removeAnnotation(
occurrenceAnnotations[i]);
}
}
occurrenceAnnotations= null;
}
}
private Position[] convertRefNodesToPositions(List<Node> refs) {
List<Position> positions = new ArrayList<>(refs.size());
for (Iterator<Node> iter=refs.iterator();
iter.hasNext();
) {
Node node = iter.next();
Node identifyingNode = getIdentifyingNode(node);
if (identifyingNode != null) {
positions.add(new Position(
identifyingNode.getStartIndex(),
identifyingNode.getDistance()));
}
}
Position[] result = new Position[positions.size()];
return positions.toArray(result);
}
private Tree.CompilationUnit 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).
return parseController.getLastCompilationUnit();
}
private IEditorInput getEditorInput() {
return activeEditor.getEditorInput();
}
private IDocumentProvider getDocumentProvider() {
documentProvider =
activeEditor.getDocumentProvider();
return documentProvider;
}
private void setUpActiveEditor(CeylonEditor textEditor) {
unregisterListeners();
if (textEditor == null) {
return;
}
activeEditor = textEditor;
document = getDocumentFromEditor();
parseController =
activeEditor.getParseController();
if (parseController == null) {
return;
}
registerListeners();
ISelection selection =
activeEditor.getSelectionProvider()
.getSelection();
if (selection instanceof ITextSelection) {
ITextSelection textSelection =
(ITextSelection) selection;
recomputeAnnotationsForSelection(
textSelection.getOffset(),
textSelection.getLength(),
document);
}
}
private Object getLockObject(IAnnotationModel annotationModel) {
if (annotationModel instanceof ISynchronizable) {
ISynchronizable sync =
(ISynchronizable) annotationModel;
return sync.getLockObject();
}
else {
return annotationModel;
}
}
@Override
public void selectionChanged(IAction action,
ISelection selection) {}
@Override
public void dispose() {
unregisterListeners();
}
@Override
public void init(IWorkbenchWindow window) {
window.getActivePage()
.addPartListener(new EditorPartListener());
}
@Override
public void update(CeylonParseController parseController,
IProgressMonitor monitor) {
if (activeEditor!=null) {
// synchronized (activeEditor) {
if (!activeEditor.isBackgroundParsingPaused() &&
!activeEditor.isInLinkedMode()) {
try {
activeEditor.getEditorSite()
.getShell()
.getDisplay()
.asyncExec(
new Runnable() {
@Override
public void run() {
if (activeEditor!=null) {
IRegion selection =
activeEditor.getSelection();
recomputeAnnotationsForSelection(
selection.getOffset(),
selection.getLength(),
document);
}
}
});
}
catch (Exception e) {
e.printStackTrace();
}
}
// }
}
}
@Override
public Stage getStage() {
return Stage.TYPE_ANALYSIS;
}
private List<Node> getDeclarationsOf(
CeylonParseController controller, Node node) {
if (controller.getStage().ordinal()
>= getStage().ordinal()) {
// Check whether we even have an AST in which to find occurrences
Tree.CompilationUnit root =
controller.getLastCompilationUnit();
if (root!=null) {
Referenceable declaration =
getReferencedExplicitDeclaration(
node, root);
if (declaration!=null) {
List<Node> occurrences =
new ArrayList<Node>();
FindReferencesVisitor frv =
new FindReferencesVisitor(
declaration);
FindDeclarationNodeVisitor fdv =
new FindDeclarationNodeVisitor(
frv.getDeclaration());
root.visit(fdv);
Tree.StatementOrArgument decNode =
fdv.getDeclarationNode();
if (decNode!=null) {
occurrences.add(decNode);
}
return occurrences;
}
}
}
return emptyList();
}
private List<Node> getOccurrencesOf(
CeylonParseController controller, Node node) {
if (controller.getStage().ordinal()
>= getStage().ordinal()) {
// Check whether we even have an AST in which to find occurrences
Tree.CompilationUnit root =
controller.getLastCompilationUnit();
if (root!=null) {
Referenceable declaration =
getReferencedExplicitDeclaration(
node, root);
if (declaration!=null) {
List<Node> occurrences =
new ArrayList<Node>();
FindReferencesVisitor frv =
new FindReferencesVisitor(
declaration);
root.visit(frv);
occurrences.addAll(frv.getReferenceNodeSet());
return occurrences;
}
}
}
return emptyList();
}
private List<Node> getAssignmentsOf(
CeylonParseController controller, Node node) {
// Check whether we even have an AST in which to find occurrences
Tree.CompilationUnit root =
controller.getLastCompilationUnit();
if (root!=null) {
Referenceable declaration =
getReferencedExplicitDeclaration(
node, root);
if (declaration instanceof Declaration) {
List<Node> occurrences =
new ArrayList<Node>();
FindAssignmentsVisitor frv =
new FindAssignmentsVisitor(
(Declaration) declaration);
root.visit(frv);
occurrences.addAll(frv.getAssignmentNodeSet());
return occurrences;
}
}
return emptyList();
}
}