blob: 5361998d389e8ed3f68535190699bcfbd6eb3300 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2005, 2021 Stephan Wahlbrink 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, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# 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.Map;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jface.text.AbstractDocument;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPartitioningException;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITypedRegion;
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.swt.graphics.Point;
import org.eclipse.ui.texteditor.IDocumentProvider;
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.ecommons.text.core.PartitionConstraint;
import org.eclipse.statet.ltk.ast.core.util.AstSelection;
import org.eclipse.statet.ltk.core.SourceModelStamp;
import org.eclipse.statet.ltk.model.core.ModelManager;
import org.eclipse.statet.ltk.model.core.element.SourceUnit;
import org.eclipse.statet.ltk.model.core.element.SourceUnitModelInfo;
import org.eclipse.statet.ltk.ui.LTKInputData;
import org.eclipse.statet.ltk.ui.SelectionWithElementInfoListener;
@NonNullByDefault
public abstract class AbstractMarkOccurrencesProvider implements SourceEditorAddon,
SelectionWithElementInfoListener {
private static final int CLEAR= -1;
private static final int KEEP= 1;
private static final int UPDATE= 2;
public final class RunData {
public final AbstractDocument doc;
public SourceModelStamp stamp;
private @NonNull Annotation[] annotations;
private @Nullable Point range;
private int set= 0;
private @Nullable Map<Annotation, Position> todo;
RunData(final AbstractDocument doc, final SourceModelStamp stamp) {
this.doc= doc;
this.stamp= stamp;
}
public boolean isValid() {
final Point currentSelection= AbstractMarkOccurrencesProvider.this.editor.currentSelection;
final var range= this.range;
return (range != null && currentSelection.x >= range.x
&& currentSelection.x + currentSelection.y <= range.y
&& this.doc.getModificationStamp() == this.stamp.getContentStamp() );
}
public boolean accept(final Point range) {
this.range= range;
if (isValid()) {
return true;
}
this.range= null;
return false;
}
public void set(final Map<Annotation, Position> annotations) {
this.set= UPDATE;
this.todo= annotations;
}
public void keep() {
this.set= KEEP;
}
public void clear() {
this.set= CLEAR;
}
}
private final SourceEditor1 editor;
private final String partitioning;
private final PartitionConstraint toleratePartitions;
private boolean isMarkEnabled;
private @Nullable RunData lastRun;
public AbstractMarkOccurrencesProvider(final SourceEditor1 editor,
final PartitionConstraint toleratePartitions) {
this.editor= nonNullAssert(editor);
this.partitioning= this.editor.getDocumentContentInfo().getPartitioning();
this.toleratePartitions= nonNullAssert(toleratePartitions);
}
@Override
public void install(final SourceEditor editor) {
this.isMarkEnabled= true;
this.editor.addPostSelectionWithElementInfoListener(this);
}
@Override
public void uninstall() {
this.isMarkEnabled= false;
this.editor.removePostSelectionWithElementInfoListener(this);
removeAnnotations();
}
@Override
public void inputChanged() {
this.lastRun= null;
}
@Override
public void stateChanged(final LTKInputData state) {
final ISelection selection= state.getSelection();
final boolean ok= update((SourceUnit)state.getInputElement(), state.getAstSelection(),
(selection instanceof ITextSelection) ? (ITextSelection)selection : null );
if (!ok && state.isStillValid()) {
removeAnnotations();
}
}
/**
* Updates the occurrences annotations based on the current selection.
*
* @return <code>true</code> if the annotation is ok (still valid or updated),
* otherwise <code>false</code>
*/
protected boolean update(final SourceUnit inputElement,
final @Nullable AstSelection astSelection, final @Nullable ITextSelection orgSelection) {
if (!this.isMarkEnabled) {
return false;
}
try {
final SourceUnitModelInfo info= inputElement.getModelInfo(this.editor.getModelTypeId(),
ModelManager.NONE, new NullProgressMonitor() );
if (this.editor.getSourceUnit() != inputElement || info == null || astSelection == null) {
return false;
}
final RunData run= new RunData(inputElement.getDocument(null), info.getStamp());
if (run.doc == null) {
return false;
}
final var lastRun= this.lastRun;
if (lastRun != null && lastRun.isValid() && lastRun.stamp.equals(run.stamp)) {
return true;
}
doUpdate(run, info, astSelection, orgSelection);
if (!this.isMarkEnabled) {
return false;
}
if (run.set == 0) {
checkKeep(run, orgSelection);
}
switch (run.set) {
case KEEP:
return true;
case UPDATE:
updateAnnotations(run);
return true;
default:
removeAnnotations();
return true;
}
}
catch (final BadLocationException e) {
}
catch (final BadPartitioningException e) {
}
catch (final UnsupportedOperationException e) {
}
return false;
}
protected abstract void doUpdate(RunData run, SourceUnitModelInfo info,
AstSelection astSelection, @Nullable ITextSelection orgSelection)
throws BadLocationException, BadPartitioningException, UnsupportedOperationException;
protected void checkKeep(final RunData run, final @Nullable ITextSelection selection)
throws BadLocationException, BadPartitioningException {
final var lastRun= this.lastRun;
if (lastRun == null || !lastRun.stamp.equals(run.stamp)) {
run.clear();
return;
}
if (selection instanceof ITextSelection) {
final ITextSelection textSelection= selection;
final Point currentSelection= this.editor.currentSelection;
final int offset= textSelection.getOffset();
final int docLength= run.doc.getLength();
final ITypedRegion partition= run.doc.getPartition(this.partitioning, offset, false);
if (docLength > 0 &&
( (currentSelection.y > 0)
|| (offset != currentSelection.x)
|| (textSelection.getLength() == 0
&& partition != null && this.toleratePartitions.matches(partition.getType())
&& (offset <= 0 || !Character.isLetterOrDigit(run.doc.getChar(offset-1)) )
&& (offset >= docLength || Character.isWhitespace(run.doc.getChar(offset)) ) )
)) {
run.keep();
return;
}
}
return;
}
protected IAnnotationModel getAnnotationModel() {
final IDocumentProvider documentProvider= this.editor.getDocumentProvider();
if (documentProvider == null) {
throw new UnsupportedOperationException();
}
final IAnnotationModel annotationModel= documentProvider.getAnnotationModel(
this.editor.getEditorInput() );
if (annotationModel == null || !(annotationModel instanceof IAnnotationModelExtension)) {
throw new UnsupportedOperationException();
}
return annotationModel;
}
protected void updateAnnotations(final RunData run) throws BadLocationException {
if (!run.isValid()) {
return;
}
// Add occurrence annotations
final IAnnotationModel annotationModel= getAnnotationModel();
final var lastRun= this.lastRun;
final Map<Annotation, Position> annotationMap= nonNullAssert(run.todo);
// create diff ?
// if (lastRun != null && Arrays.equals(run.name, lastRun.name)) {
// }
run.annotations= annotationMap.keySet().toArray(new @NonNull Annotation[annotationMap.keySet().size()]);
run.todo= null;
synchronized (SourceEditor1.getLockObject(annotationModel)) {
if (!run.isValid()) {
return;
}
((IAnnotationModelExtension)annotationModel).replaceAnnotations(
(lastRun != null) ? lastRun.annotations : null, annotationMap );
this.lastRun= run;
}
}
protected void removeAnnotations() {
final IAnnotationModel annotationModel= getAnnotationModel();
synchronized (SourceEditor1.getLockObject(annotationModel)) {
final var lastRun= this.lastRun;
if (lastRun == null) {
return;
}
this.lastRun= null;
((IAnnotationModelExtension)annotationModel).replaceAnnotations(lastRun.annotations, null);
}
}
}