| /*=============================================================================# |
| # 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 | BadPartitioningException |
| | 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); |
| } |
| } |
| |
| } |