blob: 120c86346f01a88172bcbce5f0b2ae1255d88c13 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2015 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
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.ui.texteditor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.osgi.framework.Bundle;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResourceStatus;
import org.eclipse.core.filebuffers.IPersistableAnnotationModel;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.AnnotationModel;
import org.eclipse.jface.text.source.IAnnotationMap;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.internal.editors.text.EditorsPlugin;
import org.eclipse.ui.internal.editors.text.NLSUtility;
import org.eclipse.ui.editors.text.EditorsUI;
/**
* Abstract implementation of a marker-based annotation model.
* <p>
* Markers are provided by an underlying source (a subclass responsibility).
* Markers whose textual range gets deleted during text editing are removed
* from the model on save. The {@link #updateMarkers(IDocument)} method can be used
* to force the model to update the source's markers with any changes to their
* locations due to edits. Clients can register a {@link org.eclipse.ui.texteditor.IMarkerUpdater}
* objects in order to define the process of marker updating. Registration can be done
* using the <code>"org.eclipse.ui.markerUpdaters"</code> extension point.
* </p>
* <p>
* Subclasses must implement the following methods:
* <ul>
* <li><code>retrieveMarkers</code></li>
* <li><code>isAcceptable</code></li>
* <li><code>deleteMarkers</code></li>
* <li><code>listenToMarkerChanges</code></li>
* </ul>
* </p>
*/
public abstract class AbstractMarkerAnnotationModel extends AnnotationModel implements IPersistableAnnotationModel {
/** List of annotations whose text range became invalid because of document changes */
private List<Annotation> fDeletedAnnotations= new ArrayList<>(2);
/** List of registered and instantiated marker updaters */
private List<IMarkerUpdater> fInstantiatedMarkerUpdaters= null;
/** List of registered but not yet instantiated marker updaters */
private List<IConfigurationElement> fMarkerUpdaterSpecifications= null;
private static final String ID= "id"; //$NON-NLS-1$
/**
* Retrieves all markers from this model.
* <p>
* Subclasses must implement this method.</p>
*
* @return the list of markers
* @throws CoreException if there is a problem getting the markers
*/
protected abstract IMarker[] retrieveMarkers() throws CoreException;
/**
* Deletes the given markers from this model.
* <p>
* Subclasses must implement this method.</p>
*
* @param markers the array of markers
* @throws CoreException if there are problems deleting the markers
*/
protected abstract void deleteMarkers(IMarker[] markers) throws CoreException;
/**
* Tells the model whether it should listen for marker changes.
* <p>
* Subclasses must implement this method.</p>
*
* @param listen <code>true</code> if this model should listen, and
* <code>false</code> otherwise
*/
protected abstract void listenToMarkerChanges(boolean listen);
/**
* Determines whether the marker is acceptable as an addition to this model.
* If the marker, say, represents an aspect or range of no interest to this
* model, the marker is rejected.
* <p>
* Subclasses must implement this method.</p>
*
* @param marker the marker
* @return <code>true</code> if the marker is acceptable
*/
protected abstract boolean isAcceptable(IMarker marker);
/**
* Creates a new annotation model. The annotation model does not manage any
* annotations and is not connected to any document.
*/
protected AbstractMarkerAnnotationModel() {
}
/**
* Adds the given marker updater to this annotation model.
* It is the client's responsibility to ensure the consistency
* of the set of registered marker updaters.
*
* @param markerUpdater the marker updater to be added
*/
protected void addMarkerUpdater(IMarkerUpdater markerUpdater) {
if (!fInstantiatedMarkerUpdaters.contains(markerUpdater))
fInstantiatedMarkerUpdaters.add(markerUpdater);
}
/**
* Removes the given marker updater from this annotation model.
*
* @param markerUpdater the marker updater to be removed
*/
protected void removeMarkerUpdater(IMarkerUpdater markerUpdater) {
fInstantiatedMarkerUpdaters.remove(markerUpdater);
}
/**
* Creates a new annotation for the given marker.
* <p>
* Subclasses may override.</p>
*
* @param marker the marker
* @return the new marker annotation
*/
protected MarkerAnnotation createMarkerAnnotation(IMarker marker) {
return new MarkerAnnotation(marker);
}
/**
* Handles an unanticipated <code>CoreException</code> in
* a standard manner.
*
* @param exception the exception
* @param message a message to aid debugging
*/
protected void handleCoreException(CoreException exception, String message) {
Bundle bundle = Platform.getBundle(PlatformUI.PLUGIN_ID);
ILog log= Platform.getLog(bundle);
if (message != null)
log.log(new Status(IStatus.ERROR, PlatformUI.PLUGIN_ID, IStatus.OK, message, exception));
else
log.log(exception.getStatus());
}
/**
* Creates and returns the character position of the given marker based
* on its attributes.
* <p>
* Subclasses may override.</p>
*
* @param marker the marker
* @return the new position or <code>null</code> if the marker attributes do not specify a valid position
*/
protected Position createPositionFromMarker(IMarker marker) {
int start= MarkerUtilities.getCharStart(marker);
int end= MarkerUtilities.getCharEnd(marker);
if (start > end) {
end= start + end;
start= end - start;
end= end - start;
}
if (start == -1 && end == -1) {
// marker line number is 1-based
int line= MarkerUtilities.getLineNumber(marker);
if (line > 0 && fDocument != null) {
try {
start= fDocument.getLineOffset(line - 1);
end= start;
} catch (BadLocationException x) {
}
}
}
if (start > -1 && end > -1)
return new Position(start, end - start);
return null;
}
/**
* Creates an annotation for the given marker and adds it to this model.
* Does nothing if the marker is not acceptable to this model.
*
* @param marker the marker
* @see #isAcceptable(IMarker)
*/
protected final void addMarkerAnnotation(IMarker marker) {
if (isAcceptable(marker)) {
Position p= createPositionFromMarker(marker);
if (p != null)
try {
MarkerAnnotation annotation= createMarkerAnnotation(marker);
if (annotation != null)
addAnnotation(annotation, p, false);
} catch (BadLocationException e) {
// ignore invalid position
}
}
}
/**
* Connects to the source of markers as marker change listener.
* @see AnnotationModel#connected()
*/
@Override
protected void connected() {
listenToMarkerChanges(true);
try {
catchupWithMarkers();
} catch (CoreException x) {
if (x.getStatus().getCode() != IResourceStatus.RESOURCE_NOT_FOUND)
handleCoreException(x, TextEditorMessages.AbstractMarkerAnnotationModel_connected);
}
fireModelChanged();
}
/**
* Installs all marker updaters for this marker annotation model.
*/
private void installMarkerUpdaters() {
// initialize lists - indicates that the initialization happened
fInstantiatedMarkerUpdaters= new ArrayList<>(2);
HashMap<String, Integer> markerUpdaterOrderMap = new HashMap<>(2);
LinkedList<IConfigurationElement> markerUpdaterSpecificationsLinkedList= new LinkedList<>();
// populate list
IExtensionPoint extensionPoint= Platform.getExtensionRegistry().getExtensionPoint(EditorsUI.PLUGIN_ID, "markerUpdaters"); //$NON-NLS-1$
if (extensionPoint != null) {
IConfigurationElement[] elements= extensionPoint.getConfigurationElements();
for (int i= 0; i < elements.length; i++) {
markerUpdaterSpecificationsLinkedList.add(elements[i]);
markerUpdaterOrderMap.put(elements[i].getAttribute(ID), new Integer(i));
}
//start sorting based on required-updater definition
HashMap<String, ArrayList<String>> markerUpdaterRequiredByOrderMap= new HashMap<>(2);
for (int i= 0; i < elements.length; i++) {
// Required marker should execute before other updater markers
IConfigurationElement[] requiredUpdaters= elements[i].getChildren("required-updater"); //$NON-NLS-1$
if (requiredUpdaters.length > 0) {
//ArrayList requiredUpdaters= new ArrayList(2);
for (int j= 0; j < requiredUpdaters.length; j++) { // If required updaters have been defined
String requiredID= requiredUpdaters[j].getAttribute(ID);
// If required ID is not a valid id
if (requiredID == null || (markerUpdaterOrderMap.get(requiredID) == null)) { // ID missing or invalid - log the message and move to next contribution
String msg= NLSUtility.format(TextEditorMessages.AbstractMarkerAnnotationModel_updaterInvalidDefinition, new Object[] { elements[i].getAttribute(ID), requiredID });
EditorsPlugin.log(new Status(IStatus.ERROR, PlatformUI.PLUGIN_ID, msg));
continue;
}
// Updating requiredByUpdaters to identify and log error for cyclic Dependency like A required B, B required C, C required D and D required A
// or A requires B and B requires A
ArrayList<String> requiredByUpdaters;
if (markerUpdaterRequiredByOrderMap.get(requiredID) == null) {
requiredByUpdaters= new ArrayList<>(2);
}
else {
requiredByUpdaters= markerUpdaterRequiredByOrderMap.get(requiredID);
}
// Build up extended required id list to identify Case 2
if (markerUpdaterRequiredByOrderMap.get(elements[i].getAttribute(ID)) != null) {
ArrayList<String> requiredByList= markerUpdaterRequiredByOrderMap.get(elements[i].getAttribute(ID));
requiredByUpdaters.addAll(requiredByList);
}
if (requiredByUpdaters.contains(requiredID)) { //log error if marker ID is in the required list of required ID
String msg= NLSUtility.format(TextEditorMessages.AbstractMarkerAnnotationModel_markerUpdaterCyclicDefinition, new Object[] { elements[i].getAttribute(ID), requiredID });
EditorsPlugin.log(new Status(IStatus.ERROR, PlatformUI.PLUGIN_ID, msg));
continue;
}
requiredByUpdaters.add(elements[i].getAttribute(ID));
markerUpdaterRequiredByOrderMap.put(requiredID, requiredByUpdaters);
Integer requiredLocation= markerUpdaterOrderMap.get(requiredID);
if (requiredLocation.intValue() > markerUpdaterOrderMap.get(elements[i].getAttribute(ID)).intValue()) { // If required marker is not ordered before
int newLocation= (markerUpdaterOrderMap.get(elements[i].getAttribute(ID)).intValue() == 0) ? 0 : (markerUpdaterOrderMap.get(elements[i]
.getAttribute(ID)).intValue() - 1);
IConfigurationElement requiredMarker= markerUpdaterSpecificationsLinkedList.remove(requiredLocation.intValue());
markerUpdaterSpecificationsLinkedList.add(newLocation, requiredMarker); // Put the required location before the marker
markerUpdaterOrderMap.put(requiredID, new Integer(newLocation));
markerUpdaterOrderMap.put(elements[i].getAttribute(ID), new Integer(newLocation + 1));
}
}
}
}
fMarkerUpdaterSpecifications= new ArrayList<>(markerUpdaterSpecificationsLinkedList);
//end sorting
}
}
/**
* Uninstalls all marker updaters.
*/
private void uninstallMarkerUpdaters() {
if (fInstantiatedMarkerUpdaters != null) {
fInstantiatedMarkerUpdaters.clear();
fInstantiatedMarkerUpdaters= null;
}
if (fMarkerUpdaterSpecifications != null) {
fMarkerUpdaterSpecifications.clear();
fMarkerUpdaterSpecifications= null;
}
}
/**
* Removes the marker change listener.
* @see AnnotationModel#disconnected()
*/
@Override
protected void disconnected() {
listenToMarkerChanges(false);
uninstallMarkerUpdaters();
}
/**
* Returns the position known to this annotation model for the given marker.
*
* @param marker the marker
* @return the position, or <code>null</code> if none
*/
public Position getMarkerPosition(IMarker marker) {
MarkerAnnotation a= getMarkerAnnotation(marker);
if (a != null) {
return getAnnotationMap().get(a);
}
return null;
}
/**
* Updates the annotation corresponding to the given marker which has changed
* in some way.
* <p>
* Subclasses may override.</p>
*
* @param marker the marker
*/
protected void modifyMarkerAnnotation(IMarker marker) {
MarkerAnnotation a= getMarkerAnnotation(marker);
if (a != null) {
Position p= createPositionFromMarker(marker);
if (p != null) {
a.update();
modifyAnnotationPosition(a, p, false);
}
} else
addMarkerAnnotation(marker);
}
@Override
protected void removeAnnotations(List<? extends Annotation> annotations, boolean fireModelChanged, boolean modelInitiated) {
if (annotations != null && annotations.size() > 0) {
List<Annotation> markerAnnotations= new ArrayList<>();
for (Iterator<? extends Annotation> e= annotations.iterator(); e.hasNext();) {
Annotation a= e.next();
if (a instanceof MarkerAnnotation)
markerAnnotations.add(a);
// remove annotations from annotation model
removeAnnotation(a, false);
}
if (markerAnnotations.size() > 0) {
if (modelInitiated) {
// if model initiated also remove it from the marker manager
listenToMarkerChanges(false);
try {
IMarker[] m= new IMarker[markerAnnotations.size()];
for (int i= 0; i < m.length; i++) {
MarkerAnnotation ma = (MarkerAnnotation) markerAnnotations.get(i);
m[i]= ma.getMarker();
}
deleteMarkers(m);
} catch (CoreException x) {
handleCoreException(x, TextEditorMessages.AbstractMarkerAnnotationModel_removeAnnotations);
}
listenToMarkerChanges(true);
} else {
// remember deleted annotations in order to remove their markers later on
fDeletedAnnotations.addAll(markerAnnotations);
}
}
if (fireModelChanged)
fireModelChanged();
}
}
/**
* Removes the annotation corresponding to the given marker. Does nothing
* if there is no annotation for this marker.
*
* @param marker the marker
*/
protected final void removeMarkerAnnotation(IMarker marker) {
MarkerAnnotation a= getMarkerAnnotation(marker);
if (a != null) {
removeAnnotation(a, false);
}
}
/**
* Re-populates this model with annotations for all markers retrieved
* from the maker source via <code>retrieveMarkers</code>.
*
* @throws CoreException if there is a problem getting the markers
*/
private void catchupWithMarkers() throws CoreException {
for (Iterator<Annotation> e=getAnnotationIterator(false); e.hasNext();) {
Annotation a= e.next();
if (a instanceof MarkerAnnotation)
removeAnnotation(a, false);
}
IMarker[] markers= retrieveMarkers();
if (markers != null) {
for (int i= 0; i < markers.length; i++)
addMarkerAnnotation(markers[i]);
}
}
/**
* Returns this model's annotation for the given marker.
*
* @param marker the marker
* @return the annotation, or <code>null</code> if none
*/
public final MarkerAnnotation getMarkerAnnotation(IMarker marker) {
Iterator<Annotation> e= getAnnotationIterator(false);
while (e.hasNext()) {
Object o= e.next();
if (o instanceof MarkerAnnotation) {
MarkerAnnotation a= (MarkerAnnotation) o;
if (marker.equals(a.getMarker())) {
return a;
}
}
}
return null;
}
/**
* Creates a marker updater as specified in the given configuration element.
*
* @param element the configuration element
* @return the created marker updater or <code>null</code> if none could be created
*/
private IMarkerUpdater createMarkerUpdater(IConfigurationElement element) {
try {
return (IMarkerUpdater) element.createExecutableExtension("class"); //$NON-NLS-1$
} catch (CoreException x) {
handleCoreException(x, TextEditorMessages.AbstractMarkerAnnotationModel_createMarkerUpdater);
}
return null;
}
/**
* Checks whether a marker updater is registered for the type of the
* given marker but not yet instantiated. If so, the method instantiates
* the marker updater and registers it with this model.
*
* @param marker the marker for which to look for an updater
* @since 2.0
*/
private void checkMarkerUpdaters(IMarker marker) {
List<IConfigurationElement> toBeDeleted= new ArrayList<>();
for (int i= 0; i < fMarkerUpdaterSpecifications.size(); i++) {
IConfigurationElement spec= fMarkerUpdaterSpecifications.get(i);
String markerType= spec.getAttribute("markerType"); //$NON-NLS-1$
if (markerType == null || MarkerUtilities.isMarkerType(marker, markerType)) {
toBeDeleted.add(spec);
IMarkerUpdater updater= createMarkerUpdater(spec);
if (updater != null)
addMarkerUpdater(updater);
}
}
for (int i= 0; i < toBeDeleted.size(); i++)
fMarkerUpdaterSpecifications.remove(toBeDeleted.get(i));
}
/**
* Updates the given marker according to the given position in the given
* document. If the given position is <code>null</code>, the marker is
* assumed to carry the correct positional information. If it is detected
* that the marker is invalid and should thus be deleted, this method
* returns <code>false</code>.
* <p>
* <strong>Note:</strong> This implementation queries the registered
* {@linkplain IMarkerUpdater}s. If any of these updaters returns
* <code>false</code> this method also returns <code>false</code>.
* </p>
*
* @param marker the marker to be updated
* @param document the document into which the given position points
* @param position the current position of the marker inside the given document
* @return <code>false</code> if the marker is invalid
* @throws CoreException if there is a problem updating the marker
* @since 2.0
* @deprecated use <code>updateMarker(IDocument, IMarker, Position)</code> instead. This method will be changed to protected.
*/
@Deprecated
public boolean updateMarker(IMarker marker, IDocument document, Position position) throws CoreException {
if (fMarkerUpdaterSpecifications == null)
installMarkerUpdaters();
if (!fMarkerUpdaterSpecifications.isEmpty())
checkMarkerUpdaters(marker);
boolean isOK= true;
for (int i= 0; i < fInstantiatedMarkerUpdaters.size(); i++) {
IMarkerUpdater updater= fInstantiatedMarkerUpdaters.get(i);
String markerType= updater.getMarkerType();
if (markerType == null || MarkerUtilities.isMarkerType(marker, markerType)) {
if (position == null) {
/* compatibility code */
position= createPositionFromMarker(marker);
}
isOK= (isOK && updater.updateMarker(marker, document, position));
}
}
return isOK;
}
/**
* Updates the given marker according to the given position in the given
* document. If the given position is <code>null</code>, the marker is
* assumed to carry the correct positional information. If it is detected
* that the marker is invalid and should thus be deleted, this method
* returns <code>false</code>.
*
* @param marker the marker to be updated
* @param document the document into which the given position points
* @param position the current position of the marker inside the given document
* @return <code>false</code> if the marker is invalid
* @throws CoreException if there is a problem updating the marker
* @since 3.0
*/
public boolean updateMarker(IDocument document, IMarker marker, Position position) throws CoreException {
listenToMarkerChanges(false);
try {
return updateMarker(marker, document, position);
} finally {
listenToMarkerChanges(true);
}
}
/**
* Updates the markers managed by this annotation model by calling
* all registered marker updaters (<code>IMarkerUpdater</code>).
*
* @param document the document to which this model is currently connected
* @throws CoreException if there is a problem updating the markers
*/
public void updateMarkers(IDocument document) throws CoreException {
Assert.isTrue(fDocument == document);
IAnnotationMap annotationMap= getAnnotationMap();
if (annotationMap.size() == 0 && fDeletedAnnotations.size() == 0)
return;
if (fMarkerUpdaterSpecifications == null)
installMarkerUpdaters();
listenToMarkerChanges(false);
try {
// update all markers with the positions known by the annotation model
for (Iterator<Annotation> e= getAnnotationIterator(false); e.hasNext();) {
Object o= e.next();
if (o instanceof MarkerAnnotation) {
MarkerAnnotation a= (MarkerAnnotation) o;
IMarker marker= a.getMarker();
Position position= annotationMap.get(a);
if ( !updateMarker(marker, document, position)) {
if ( !fDeletedAnnotations.contains(a))
fDeletedAnnotations.add(a);
}
}
}
if (!fDeletedAnnotations.isEmpty()) {
removeAnnotations(fDeletedAnnotations, true, true);
fDeletedAnnotations.clear();
}
} finally {
listenToMarkerChanges(true);
}
}
/**
* Resets all the markers to their original state.
*/
public void resetMarkers() {
// re-initializes the positions from the markers
for (Iterator<Annotation> e= getAnnotationIterator(false); e.hasNext();) {
Object o= e.next();
if (o instanceof MarkerAnnotation) {
MarkerAnnotation a= (MarkerAnnotation) o;
Position p= createPositionFromMarker(a.getMarker());
if (p != null) {
removeAnnotation(a, false);
try {
addAnnotation(a, p, false);
} catch (BadLocationException e1) {
// ignore invalid position
}
}
}
}
// add the markers of deleted positions back to the annotation model
for (Iterator<Annotation> e= fDeletedAnnotations.iterator(); e.hasNext();) {
Object o= e.next();
if (o instanceof MarkerAnnotation) {
MarkerAnnotation a= (MarkerAnnotation) o;
Position p= createPositionFromMarker(a.getMarker());
if (p != null)
try {
addAnnotation(a, p, false);
} catch (BadLocationException e1) {
// ignore invalid position
}
}
}
fDeletedAnnotations.clear();
// fire annotation model changed
fireModelChanged();
}
@Override
public void commit(IDocument document) throws CoreException {
updateMarkers(document);
}
@Override
public void revert(IDocument document) {
resetMarkers();
}
@Override
public void reinitialize(IDocument document) {
resetMarkers();
}
}