/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * 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: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.ide.api.editor.annotation; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import org.eclipse.che.ide.api.editor.document.DocumentHandle; import org.eclipse.che.ide.api.editor.events.DocumentChangeEvent; import org.eclipse.che.ide.api.editor.partition.DocumentPositionMap; import org.eclipse.che.ide.api.editor.text.BadLocationException; import org.eclipse.che.ide.api.editor.text.BadPositionCategoryException; import org.eclipse.che.ide.api.editor.text.LinearRange; import org.eclipse.che.ide.api.editor.text.Position; import org.eclipse.che.ide.api.editor.text.TextPosition; import org.eclipse.che.ide.api.editor.text.annotation.Annotation; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; /** * Default implementation of {@link AnnotationModel} */ public class AnnotationModelImpl implements AnnotationModel { /** The list of managed annotations */ protected Map<Annotation, Position> annotations; /** The map which maps {@link Position} to {@link Annotation}. */ private final IdentityHashMap<Position, Annotation> positions; /** The current annotation model event. */ private AnnotationModelEvent modelEvent; private DocumentHandle docHandle; private boolean documentChanged; private final DocumentPositionMap documentPositionMap; private PositionHolder positionHolder; public AnnotationModelImpl(final DocumentPositionMap docPositionMap) { this.annotations = new HashMap<>(10); this.positions = new IdentityHashMap<>(10); this.documentPositionMap = docPositionMap; } @Override public void addAnnotation(final Annotation annotation, final Position position) { addAnnotation(annotation, position, true); } protected void addAnnotation(final Annotation annotation, final Position position, final boolean fireEvent) { annotations.put(annotation, position); positions.put(position, annotation); try { addPosition(position); } catch (BadLocationException ignore) { //ignore invalid location } getAnnotationModelEvent().annotationAdded(annotation); if (fireEvent) { fireModelChanged(); } } private void addPosition(Position position) throws BadLocationException { if (positionHolder != null) { positionHolder.addPosition(position); } } /** * Returns the current annotation model event. This is the event that will be sent out when calling <code>fireModelChanged</code>. */ protected final AnnotationModelEvent getAnnotationModelEvent() { if (this.modelEvent == null) { this.modelEvent = createAnnotationModelEvent(); } return this.modelEvent; } /** * Creates and returns a new annotation model event. Subclasses may override. * * @return a new and empty annotation model event */ protected AnnotationModelEvent createAnnotationModelEvent() { return new AnnotationModelEvent(this); } /** * Informs all annotation model listeners that this model has been changed as described in the annotation model event. */ protected void fireModelChanged() { final boolean empty = getAnnotationModelEvent().isEmpty(); if (empty) { return; } if (getDocumentHandle() == null || getDocumentHandle().getDocEventBus() == null) { return; } getDocumentHandle().getDocEventBus().fireEvent(this.modelEvent); } @Override public void removeAnnotation(final Annotation annotation) { if (this.annotations.containsKey(annotation)) { Position pos = this.annotations.get(annotation); this.annotations.remove(annotation); positions.remove(pos); getAnnotationModelEvent().annotationRemoved(annotation, pos); fireModelChanged(); } } @Override public Iterator<Annotation> getAnnotationIterator() { return this.annotations.keySet().iterator(); } @Override public Iterator<Annotation> getAnnotationIterator(final int offset, final int length, final boolean canStartBefore, final boolean canEndAfter) { return getRegionAnnotationIterator(offset, length, canStartBefore, canEndAfter); } private Iterator<Annotation> getRegionAnnotationIterator(int offset, int length, boolean canStartBefore, boolean canEndAfter) { cleanup(true); try { List<Position> positions = positionHolder.getPositions(offset, length, canStartBefore, canEndAfter); return new AnnotationsIterator(positions, this.positions); } catch (BadPositionCategoryException e) { // can happen if e.g. the document doesn't contain such a category, or when removed in a different thread return Collections.<Annotation>emptyList().iterator(); } } /** * Removes all annotations from the model whose associated positions have been deleted. If requested inform all model listeners about * the change. If requested a new thread is created for the notification of the model listeners. * * @param fireModelChanged indicates whether to notify all model listeners */ private void cleanup(final boolean fireModelChanged) { if (documentChanged) { documentChanged = false; final List<Annotation> deleted = new ArrayList<Annotation>(); final Iterator<Annotation> e = getAnnotationIterator(); while (e.hasNext()) { final Annotation annotation = e.next(); final Position pos = annotations.get(annotation); if (pos == null || pos.isDeleted()) { deleted.add(annotation); } } if (fireModelChanged) { removeAnnotations(deleted, false); if (modelEvent != null) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { fireModelChanged(); } }); } } else { removeAnnotations(deleted, fireModelChanged); } } } /** * Removes the given annotations from this model. If requested all annotation model listeners will be informed about this change. * <code>modelInitiated</code> indicates whether the deletion has been initiated by this model or by one of its clients. * * @param annotations the annotations to be removed * @param fireModelChanged indicates whether to notify all model listeners */ protected void removeAnnotations(final List< ? extends Annotation> annotations, final boolean fireModelChanged) { if (!annotations.isEmpty()) { final Iterator< ? extends Annotation> e = annotations.iterator(); while (e.hasNext()) { removeAnnotation(e.next(), false); } if (fireModelChanged) { fireModelChanged(); } } } /** * Removes the given annotation from the annotation model. If requested inform all model change listeners about this change. * * @param annotation the annotation to be removed * @param fireModelChanged indicates whether to notify all model listeners */ protected void removeAnnotation(final Annotation annotation, final boolean fireModelChanged) { if (annotations.containsKey(annotation)) { Position pos = null; pos = annotations.get(annotation); annotations.remove(annotation); positions.remove(pos); getAnnotationModelEvent().annotationRemoved(annotation, pos); if (fireModelChanged) { fireModelChanged(); } } } @Override public Position getPosition(final Annotation annotation) { final Position position = annotations.get(annotation); return position; } @Override public Map<String, String> getAnnotationDecorations() { return new HashMap<>(); } @Override public Map<String, String> getAnnotationStyle() { return new HashMap<>(); } @Override public void onDocumentChange(final DocumentChangeEvent event) { this.documentChanged = true; } @Override public void setDocumentHandle(final DocumentHandle handle) { this.docHandle = handle; this.positionHolder = new PositionHolder(handle.getDocument()); } @Override public DocumentHandle getDocumentHandle() { return this.docHandle; } // TODO evaluate: keep? public void forgetLines(final int fromLine, final int count) { forgetLines(fromLine, count, true); } // TODO evaluate: keep? public void forgetLines(int fromLine) { forgetLines(fromLine, 0, false); } // TODO evaluate: keep? private void forgetLines(final int fromLine, final int count, final boolean checkCount) { // use an iterator to have remove() final Iterator<Entry<Annotation, Position>> iterator = this.annotations.entrySet().iterator(); while (iterator.hasNext()) { final Entry<Annotation, Position> entry = iterator.next(); final Position position = entry.getValue(); final TextPosition textPos = docHandle.getDocument().getPositionFromIndex(position.getOffset()); final int line = textPos.getLine(); if (line >= fromLine && (!checkCount || line < fromLine + count)) { iterator.remove(); } } } // TODO evaluate: keep? public void shiftLines(final int fromLine, final int lineDelta, final int charDelta) { final Map<Annotation, Position> modified = new IdentityHashMap<>(); for (final Entry<Annotation, Position> entry: this.annotations.entrySet()) { final Position position = entry.getValue(); final TextPosition textPos = docHandle.getDocument().getPositionFromIndex(position.getOffset()); int horizontal; if (textPos.getLine() == fromLine) { horizontal = charDelta; } else if (textPos.getLine() >= fromLine) { horizontal = 0; } else { continue; } final TextPosition newTextPos = new TextPosition(fromLine + lineDelta, textPos.getCharacter() + horizontal); final int newOffset = docHandle.getDocument().getIndexFromPosition(newTextPos); final Position newPos = new Position(newOffset, position.getLength()); modified.put(entry.getKey(), newPos); } // merge changes in the annotartion map this.annotations.putAll(modified); } @Override public void clear() { this.annotations.clear(); this.positions.clear(); this.modelEvent = new AnnotationModelEvent(this); this.docHandle.getDocEventBus().fireEvent(new ClearAnnotationModelEvent(this)); } @Override public void onQueryAnnotations(final QueryAnnotationsEvent event) { final QueryAnnotationsEvent.QueryCallback callback = event.getCallback(); if (callback == null) { return; } final LinearRange range = event.getRange(); Iterator<Annotation> iterator; if (range == null) { iterator = getAnnotationIterator(); } else { iterator = getAnnotationIterator(range.getStartOffset(), range.getLength(), true, true); } final QueryAnnotationsEvent.AnnotationFilter filter = event.getAdditionalFilter(); final Map<Annotation, Position> result = new HashMap<>(); while (iterator.hasNext()) { final Annotation annotation = iterator.next(); if (filter.accept(annotation)) { result.put(annotation, this.annotations.get(annotation)); } } callback.respond(result); } }