blob: e791b8de720c74f7902c145e8dab2ab2fd5c10a9 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2008, 2021 IBM Corporation and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0.
#
# SPDX-License-Identifier: EPL-2.0
#
# Contributors:
# IBM Corporation - org.eclipse.jdt: initial API and implementation
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.ltk.ui.sourceediting;
import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.ui.texteditor.MarkerAnnotation;
import org.eclipse.ui.texteditor.MarkerUtilities;
import org.eclipse.ui.texteditor.ResourceMarkerAnnotationModel;
import org.eclipse.statet.jcommons.collections.ImCollections;
import org.eclipse.statet.jcommons.collections.ImList;
import org.eclipse.statet.jcommons.collections.ImSet;
import org.eclipse.statet.jcommons.lang.NonNull;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.ltk.core.Ltk;
import org.eclipse.statet.ltk.issues.core.Issue;
import org.eclipse.statet.ltk.issues.core.IssueRequestor;
import org.eclipse.statet.ltk.issues.core.IssueTypeSet;
import org.eclipse.statet.ltk.issues.core.IssueTypeSet.IssueCategory;
import org.eclipse.statet.ltk.issues.core.IssueTypeSet.ProblemCategory;
import org.eclipse.statet.ltk.issues.core.IssueTypeSet.TaskCategory;
import org.eclipse.statet.ltk.issues.core.Problem;
import org.eclipse.statet.ltk.issues.core.impl.BasicIssueRequestor;
/**
* Abstract annotation model dealing with marker annotations and temporary problems.
* Also acts as problem requester for its source unit.
*/
@NonNullByDefault
public abstract class SourceAnnotationModel extends ResourceMarkerAnnotationModel {
private static class PositionMap<V> implements Iterable<PositionMap.Entry<V>> {
static class Entry<V> {
final Position position;
ImList<V> annotations;
public Entry(final Position position, final V value) {
this.position= position;
this.annotations= ImCollections.newList(value);
}
}
private final List<Entry<V>> list= new ArrayList<>();
private int anchor= 0;
public PositionMap() {
}
private int indexOf(final Position position) {
final int length= this.list.size();
for (int i= 0; i < length; i++) {
final var entry= this.list.get(i);
if (entry.position.equals(position)) {
return i;
}
}
return -1;
}
public @Nullable ImList<V> get(final Position position) {
final int length= this.list.size();
// behind anchor
for (int i= this.anchor; i < length; i++) {
final var entry= this.list.get(i);
if (entry.position.equals(position)) {
this.anchor= i;
return entry.annotations;
}
}
// before anchor
for (int i= 0; i < this.anchor; i++) {
final var entry= this.list.get(i);
if (entry.position.equals(position)) {
this.anchor= i;
return entry.annotations;
}
}
return null;
}
@Override
public Iterator<Entry<V>> iterator() {
return this.list.iterator();
}
public void add(final Position position, final V value) {
final int index= indexOf(position);
if (index >= 0) {
final var entry= this.list.get(index);
entry.annotations= ImCollections.addElement(entry.annotations, value);
}
else {
this.list.add(new Entry<>(position, value));
}
}
public void remove(final Position position, final Object value) {
final int index= indexOf(position);
if (index >= 0) {
final var entry= this.list.get(index);
entry.annotations= ImCollections.removeElement(entry.annotations, value);
if (entry.annotations.isEmpty()) {
this.list.remove(index);
}
}
}
public void clear() {
this.list.clear();
}
}
protected class SourceAnnotationIssueRequestor extends BasicIssueRequestor {
public SourceAnnotationIssueRequestor(final IssueTypeSet issueTypeSet) {
super(issueTypeSet, Ltk.EDITOR_CONTEXT);
}
@Override
protected boolean shouldAccept(final ProblemCategory category) {
return isHandlingTemporaryProblems(category);
}
@Override
protected boolean shouldAccept(final TaskCategory category) {
return false;
}
@Override
protected void reportIssues(final @Nullable TaskBatch taskBatch,
final ImList<ProblemBatch> problemBatches)
throws CoreException {
SourceAnnotationModel.this.reportIssues(problemBatches);
}
}
private final IssueTypeSet issueTypeSet;
private final AtomicInteger reportingCounter= new AtomicInteger();
private final List<SourceIssueEditorAnnotation> editorAnnotations= new ArrayList<>();
private final PositionMap<SourceIssueMarkerAnnotation<?>> markerAnnotations= new PositionMap<>();
private @Nullable ImSet<IssueCategory<?>> reportedConfig;
private List<SourceIssueMarkerAnnotation<?>> overlaidMarkerAnnotations= new ArrayList<>();
public SourceAnnotationModel(final IResource resource, final IssueTypeSet issueTypeSet) {
super(resource);
this.issueTypeSet= issueTypeSet;
}
protected IssueTypeSet getIssueTypeSet() {
return this.issueTypeSet;
}
protected abstract boolean isHandlingTemporaryProblems(ProblemCategory issueCategory);
@Override
protected @Nullable MarkerAnnotation createMarkerAnnotation(final IMarker marker) {
String markerType= MarkerUtilities.getMarkerType(marker);
if (markerType != null) {
markerType= markerType.intern();
final var issueCategory= this.issueTypeSet.getCategory(Ltk.PERSISTENCE_CONTEXT, markerType);
if (issueCategory != null) {
final var annotationType= issueCategory.mapType(Ltk.PERSISTENCE_CONTEXT, Ltk.EDITOR_CONTEXT,
markerType );
if (annotationType != null) {
return new SourceIssueMarkerAnnotation<>(issueCategory, annotationType, marker);
}
}
}
return super.createMarkerAnnotation(marker);
}
// @Override
// protected AnnotationModelEvent createAnnotationModelEvent() {
// return new CompilationUnitAnnotationModelEvent(this, getResource());
// }
public final IssueRequestor createIssueRequestor() {
this.reportingCounter.incrementAndGet();
return doCreateIssueRequestor();
}
protected IssueRequestor doCreateIssueRequestor() {
return new SourceAnnotationIssueRequestor(getIssueTypeSet());
}
private void reportIssues(final ImList<BasicIssueRequestor.ProblemBatch> problemBatches) {
synchronized (getLockObject()) {
if (this.reportingCounter.decrementAndGet() != 0) {
return;
}
{ final Set<IssueCategory<?>> prevConfig= this.reportedConfig;
final @NonNull IssueCategory<?>[] enabledCagetories= new @NonNull IssueCategory[problemBatches.size()];
int numEnabled= 0;
for (final var problemBatch : problemBatches) {
final var issueCategory= problemBatch.getCategory();
if (problemBatch.isEnabled()) {
enabledCagetories[numEnabled++]= issueCategory;
if (prevConfig != null && !prevConfig.contains(issueCategory)) {
resetMarkerAnnotationsControl(issueCategory, true);
}
}
else {
if (prevConfig != null && prevConfig.contains(issueCategory)) {
resetMarkerAnnotationsControl(issueCategory, false);
}
}
}
this.reportedConfig= ImCollections.newIdentitySet(enabledCagetories, 0, numEnabled);
}
final var prevControlledAnnotations= this.overlaidMarkerAnnotations;
this.overlaidMarkerAnnotations= new ArrayList<>();
if (this.editorAnnotations.size() > 0) {
removeAnnotations(this.editorAnnotations, false, true);
this.editorAnnotations.clear();
}
for (final var problemBatch : problemBatches) {
final var problemTypes= nonNullAssert(
problemBatch.getCategory().getTypes(Ltk.EDITOR_CONTEXT) );
if (problemBatch.isEnabled()) {
for (final Problem problem : problemBatch.getAcceptedIssues()) {
final Position position= createPosition(problem);
if (position != null) {
try {
final var annotation= new SourceIssueEditorAnnotation(
problemBatch.getCategory(),
problemTypes.getType(problem.getSeverity()),
problem );
installMarkerAnnotationOverlays(position, annotation);
addAnnotation(annotation, position, false);
this.editorAnnotations.add(annotation);
}
catch (final BadLocationException x) {
}
}
}
}
}
if (prevControlledAnnotations != null && !prevControlledAnnotations.isEmpty()) {
prevControlledAnnotations.removeAll(this.overlaidMarkerAnnotations);
for (final var problemBatch : problemBatches) {
if (problemBatch.isEnabled()) {
removeMarkerAnnotationOverlays(prevControlledAnnotations,
problemBatch.getCategory() );
}
}
}
}
fireModelChanged();
}
protected Position createPosition(final Issue issue) {
final int start= issue.getSourceStartOffset();
final int end= issue.getSourceEndOffset();
if (start < 0 && end < 0) {
assert (start >= 0 && end >= 0);
}
return new Position(start, end-start);
}
private void installMarkerAnnotationOverlays(final Position position,
final SourceIssueEditorAnnotation problemAnnotation) {
final var annotations= this.markerAnnotations.get(position);
if (annotations != null) {
final var issueCategory= problemAnnotation.getIssueCategory();
for (final var markerAnnotation : annotations) {
if (markerAnnotation.getIssueCategory() == issueCategory) {
markerAnnotation.setOverlay(problemAnnotation);
this.overlaidMarkerAnnotations.add(markerAnnotation);
}
}
}
}
private void removeMarkerAnnotationOverlays(final List<SourceIssueMarkerAnnotation<?>> annotations,
final IssueCategory<?> issueCategory) {
for (final var markerAnnotation : annotations) {
if (markerAnnotation.getIssueCategory() == issueCategory) {
markerAnnotation.setOverlay(null);
}
}
}
private void resetMarkerAnnotationsControl(final IssueCategory<?> issueCategory,
final boolean isControlled) {
final var modelEvent= getAnnotationModelEvent();
if (isControlled) {
for (final var entry : this.markerAnnotations) {
for (final var markerAnnotation : entry.annotations) {
if (markerAnnotation.getIssueCategory() == issueCategory
&& !markerAnnotation.isControlled()) {
markerAnnotation.setOverlay(null);
modelEvent.annotationChanged(markerAnnotation);
}
}
}
}
else {
for (final var entry : this.markerAnnotations) {
for (final var markerAnnotation : entry.annotations) {
if (markerAnnotation.getIssueCategory() == issueCategory
&& markerAnnotation.isControlled()) {
markerAnnotation.disableOverlay();
modelEvent.annotationChanged(markerAnnotation);
}
}
}
}
}
@Override
protected void addAnnotation(final Annotation annotation, final Position position,
final boolean fireModelChanged) throws BadLocationException {
if (annotation instanceof SourceIssueMarkerAnnotation) {
final var markerAnnotation= (SourceIssueMarkerAnnotation<?>)annotation;
synchronized (getLockObject()) {
final var config= this.reportedConfig;
if (config != null && config.contains(markerAnnotation.getIssueCategory())) {
markerAnnotation.setOverlay(null);
this.overlaidMarkerAnnotations.add(markerAnnotation); // force check
}
this.markerAnnotations.add(position, markerAnnotation);
}
}
super.addAnnotation(annotation, position, fireModelChanged);
}
@Override
protected void removeAnnotation(final Annotation annotation, final boolean fireModelChanged) {
if (annotation instanceof SourceIssueMarkerAnnotation) {
final var markerAnnotation= (SourceIssueMarkerAnnotation<?>)annotation;
final var position= getPosition(markerAnnotation);
if (position != null) {
synchronized (getLockObject()) {
if (markerAnnotation.isControlled()) {
markerAnnotation.disableOverlay();
}
this.markerAnnotations.remove(position, markerAnnotation);
}
}
}
super.removeAnnotation(annotation, fireModelChanged);
}
@Override
protected void removeAllAnnotations(final boolean fireModelChanged) {
super.removeAllAnnotations(fireModelChanged);
synchronized (getLockObject()) {
this.markerAnnotations.clear();
}
}
}