/******************************************************************************* * Copyright (c) 2006, 2016 Mountainminds GmbH & Co. KG and Contributors * 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: * Marc R. Hoffmann - initial API and implementation * ******************************************************************************/ package com.mountainminds.eclemma.internal.ui.annotation; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.ISourceReference; import org.eclipse.jdt.core.JavaModelException; 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.IRegion; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.AnnotationModelEvent; import org.eclipse.jface.text.source.IAnnotationModel; import org.eclipse.jface.text.source.IAnnotationModelExtension; import org.eclipse.jface.text.source.IAnnotationModelListener; import org.eclipse.jface.text.source.IAnnotationModelListenerExtension; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.texteditor.IDocumentProvider; import org.eclipse.ui.texteditor.ITextEditor; import org.jacoco.core.analysis.ICounter; import org.jacoco.core.analysis.ICoverageNode; import org.jacoco.core.analysis.ILine; import org.jacoco.core.analysis.ISourceNode; import com.mountainminds.eclemma.core.CoverageTools; import com.mountainminds.eclemma.core.analysis.IJavaCoverageListener; import com.mountainminds.eclemma.internal.ui.EclEmmaUIPlugin; /** * IAnnotationModel implementation for efficient coverage highlighting. */ public final class CoverageAnnotationModel implements IAnnotationModel { /** Key used to piggyback our model to the editor's model. */ private static final Object KEY = new Object(); /** List of current CoverageAnnotation objects */ private List<CoverageAnnotation> annotations = new ArrayList<CoverageAnnotation>( 32); /** List of registered IAnnotationModelListener */ private List<IAnnotationModelListener> annotationModelListeners = new ArrayList<IAnnotationModelListener>( 2); private final ITextEditor editor; private final IDocument document; private int openConnections = 0; private boolean annotated = false; private IJavaCoverageListener coverageListener = new IJavaCoverageListener() { public void coverageChanged() { updateAnnotations(true); } }; private IDocumentListener documentListener = new IDocumentListener() { public void documentChanged(DocumentEvent event) { updateAnnotations(false); } public void documentAboutToBeChanged(DocumentEvent event) { } }; private CoverageAnnotationModel(ITextEditor editor, IDocument document) { this.editor = editor; this.document = document; updateAnnotations(true); } /** * Attaches a coverage annotation model for the given editor if the editor can * be annotated. Does nothing if the model is already attached. * * @param editor * Editor to attach a annotation model to */ public static void attach(ITextEditor editor) { IDocumentProvider provider = editor.getDocumentProvider(); // there may be text editors without document providers (SF #1725100) if (provider == null) return; IAnnotationModel model = provider.getAnnotationModel(editor .getEditorInput()); if (!(model instanceof IAnnotationModelExtension)) return; IAnnotationModelExtension modelex = (IAnnotationModelExtension) model; IDocument document = provider.getDocument(editor.getEditorInput()); CoverageAnnotationModel coveragemodel = (CoverageAnnotationModel) modelex .getAnnotationModel(KEY); if (coveragemodel == null) { coveragemodel = new CoverageAnnotationModel(editor, document); modelex.addAnnotationModel(KEY, coveragemodel); } } /** * Detaches the coverage annotation model from the given editor. If the editor * does not have a model attached, this method does nothing. * * @param editor * Editor to detach the annotation model from */ public static void detach(ITextEditor editor) { IDocumentProvider provider = editor.getDocumentProvider(); // there may be text editors without document providers (SF #1725100) if (provider == null) return; IAnnotationModel model = provider.getAnnotationModel(editor .getEditorInput()); if (!(model instanceof IAnnotationModelExtension)) return; IAnnotationModelExtension modelex = (IAnnotationModelExtension) model; modelex.removeAnnotationModel(KEY); } private void updateAnnotations(boolean force) { final ISourceNode coverage = findSourceCoverageForEditor(); if (coverage != null) { if (!annotated || force) { createAnnotations(coverage); annotated = true; } } else { if (annotated) { clear(); annotated = false; } } } private ISourceNode findSourceCoverageForEditor() { if (editor.isDirty()) { return null; } final IEditorInput input = editor.getEditorInput(); if (input == null) { return null; } final Object element = input.getAdapter(IJavaElement.class); if (!hasSource((IJavaElement) element)) { return null; } return findSourceCoverageForElement(element); } private boolean hasSource(IJavaElement element) { if (element instanceof ISourceReference) { try { return ((ISourceReference) element).getSourceRange() != null; } catch (JavaModelException ex) { // we ignore this, the resource seems to have problems } } return false; } private ISourceNode findSourceCoverageForElement(Object element) { // Do we have a coverage info for the editor input? ICoverageNode coverage = CoverageTools.getCoverageInfo(element); if (coverage instanceof ISourceNode) { return (ISourceNode) coverage; } return null; } private void clear() { AnnotationModelEvent event = new AnnotationModelEvent(this); clear(event); fireModelChanged(event); } private void clear(AnnotationModelEvent event) { for (final CoverageAnnotation ca : annotations) { event.annotationRemoved(ca, ca.getPosition()); } annotations.clear(); } private void createAnnotations(final ISourceNode linecoverage) { AnnotationModelEvent event = new AnnotationModelEvent(this); clear(event); final int firstline = linecoverage.getFirstLine(); final int lastline = Math.min(linecoverage.getLastLine(), document.getNumberOfLines()); try { for (int l = firstline; l <= lastline; l++) { final ILine line = linecoverage.getLine(l); if (line.getStatus() != ICounter.EMPTY) { final IRegion region = document.getLineInformation(l - 1); final CoverageAnnotation ca = new CoverageAnnotation( region.getOffset(), region.getLength(), line); annotations.add(ca); event.annotationAdded(ca); } } } catch (BadLocationException ex) { EclEmmaUIPlugin.log(ex); } fireModelChanged(event); } public void addAnnotationModelListener(IAnnotationModelListener listener) { if (!annotationModelListeners.contains(listener)) { annotationModelListeners.add(listener); fireModelChanged(new AnnotationModelEvent(this, true)); } } public void removeAnnotationModelListener(IAnnotationModelListener listener) { annotationModelListeners.remove(listener); } private void fireModelChanged(AnnotationModelEvent event) { event.markSealed(); if (!event.isEmpty()) { for (final IAnnotationModelListener l : annotationModelListeners) { if (l instanceof IAnnotationModelListenerExtension) { ((IAnnotationModelListenerExtension) l).modelChanged(event); } else { l.modelChanged(this); } } } } public void connect(IDocument document) { if (this.document != document) { throw new IllegalArgumentException("Can't connect to different document."); //$NON-NLS-1$ } for (final CoverageAnnotation ca : annotations) { try { document.addPosition(ca.getPosition()); } catch (BadLocationException ex) { EclEmmaUIPlugin.log(ex); } } if (openConnections++ == 0) { CoverageTools.addJavaCoverageListener(coverageListener); document.addDocumentListener(documentListener); } } public void disconnect(IDocument document) { if (this.document != document) { throw new IllegalArgumentException( "Can't disconnect from different document."); //$NON-NLS-1$ } for (final CoverageAnnotation ca : annotations) { document.removePosition(ca.getPosition()); } if (--openConnections == 0) { CoverageTools.removeJavaCoverageListener(coverageListener); document.removeDocumentListener(documentListener); } } /** * External modification is not supported. */ public void addAnnotation(Annotation annotation, Position position) { throw new UnsupportedOperationException(); } /** * External modification is not supported. */ public void removeAnnotation(Annotation annotation) { throw new UnsupportedOperationException(); } public Iterator getAnnotationIterator() { return annotations.iterator(); } public Position getPosition(Annotation annotation) { if (annotation instanceof CoverageAnnotation) { return ((CoverageAnnotation) annotation).getPosition(); } else { return null; } } }