blob: d082b57790e456fc9e1f8f180d98559b5fbb85ca [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2017, 2018 Red Hat Inc. 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:
* - Lucas Bullen (Red Hat Inc.)
*******************************************************************************/
package org.eclipse.ui.internal.genericeditor;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ISynchronizable;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.reconciler.DirtyRegion;
import org.eclipse.jface.text.reconciler.IReconcilingStrategy;
import org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension;
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.text.source.ISourceViewer;
import org.eclipse.jface.viewers.IPostSelectionProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.osgi.util.NLS;
/**
*
* This Reconciler Strategy is a default stategy which will be present if no
* other highlightReconcilers are registered for a given content-type. It splits
* the text into 'words' (which are defined as anything in-between
* non-alphanumeric characters) and searches the document highlighting all like
* words.
*
* E.g. if your file contains "t^he dog in the bog" and you leave your caret at
* ^ you will get both instances of 'the' highlighted.
*
*/
public class DefaultWordHighlightStrategy
implements IReconcilingStrategy, IReconcilingStrategyExtension, IPreferenceChangeListener {
private static final String ANNOTATION_TYPE = "org.eclipse.ui.genericeditor.text"; //$NON-NLS-1$
private boolean enabled;
private ISourceViewer sourceViewer;
private IDocument document;
private static final String WORD_REGEXP = "\\w+"; //$NON-NLS-1$
private static final Pattern WORD_PATTERN = Pattern.compile(WORD_REGEXP, Pattern.UNICODE_CHARACTER_CLASS);
private static final Pattern CURRENT_WORD_START_PATTERN = Pattern.compile(WORD_REGEXP + "$", //$NON-NLS-1$
Pattern.UNICODE_CHARACTER_CLASS);
private Annotation[] fOccurrenceAnnotations = null;
private ISelectionChangedListener editorSelectionChangedListener = event -> applyHighlights(event.getSelection());
private void applyHighlights(ISelection selection) {
if (!(selection instanceof ITextSelection)) {
return;
}
ITextSelection textSelection = (ITextSelection) selection;
if (sourceViewer == null || !enabled) {
removeOccurrenceAnnotations();
return;
}
String text = document.get();
int offset = ((ITextViewerExtension5) sourceViewer).widgetOffset2ModelOffset(textSelection.getOffset());
String word = findCurrentWord(text, offset);
if (word == null) {
removeOccurrenceAnnotations();
return;
}
Matcher m = WORD_PATTERN.matcher(text);
Map<Annotation, Position> annotationMap = new HashMap<>();
while (m.find()) {
if (m.group().equals(word)) {
annotationMap.put(
new Annotation(ANNOTATION_TYPE, false,
NLS.bind(Messages.DefaultWordHighlightStrategy_OccurrencesOf, word)),
new Position(m.start(), m.end() - m.start()));
}
}
if (annotationMap.size() < 2) {
removeOccurrenceAnnotations();
return;
}
IAnnotationModel annotationModel = sourceViewer.getAnnotationModel();
synchronized (getLockObject(annotationModel)) {
if (annotationModel instanceof IAnnotationModelExtension) {
((IAnnotationModelExtension) annotationModel).replaceAnnotations(fOccurrenceAnnotations, annotationMap);
} else {
removeOccurrenceAnnotations();
Iterator<Entry<Annotation, Position>> iter = annotationMap.entrySet().iterator();
while (iter.hasNext()) {
Entry<Annotation, Position> mapEntry = iter.next();
annotationModel.addAnnotation(mapEntry.getKey(), mapEntry.getValue());
}
}
fOccurrenceAnnotations = annotationMap.keySet().toArray(new Annotation[annotationMap.keySet().size()]);
}
}
private static String findCurrentWord(String text, int offset) {
String wordStart = null;
String wordEnd = null;
String substring = text.substring(0, offset);
Matcher m = CURRENT_WORD_START_PATTERN.matcher(substring);
if (m.find()) {
wordStart = m.group();
}
substring = text.substring(offset);
m = WORD_PATTERN.matcher(substring);
if (m.lookingAt()) {
wordEnd = m.group();
}
if (wordStart != null && wordEnd != null)
return wordStart + wordEnd;
if (wordStart != null)
return wordStart;
return wordEnd;
}
public void install(ITextViewer viewer) {
if (!(viewer instanceof ISourceViewer)) {
return;
}
IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode(GenericEditorPlugin.BUNDLE_ID);
preferences.addPreferenceChangeListener(this);
this.enabled = preferences.getBoolean(ToggleHighlight.TOGGLE_HIGHLIGHT_PREFERENCE, true);
this.sourceViewer = (ISourceViewer) viewer;
((IPostSelectionProvider) sourceViewer.getSelectionProvider())
.addPostSelectionChangedListener(editorSelectionChangedListener);
}
public void uninstall() {
if (sourceViewer != null) {
((IPostSelectionProvider) sourceViewer.getSelectionProvider())
.removePostSelectionChangedListener(editorSelectionChangedListener);
}
IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode(GenericEditorPlugin.BUNDLE_ID);
preferences.removePreferenceChangeListener(this);
}
@Override
public void preferenceChange(PreferenceChangeEvent event) {
if (event.getKey().equals(ToggleHighlight.TOGGLE_HIGHLIGHT_PREFERENCE)) {
this.enabled = Boolean.parseBoolean(event.getNewValue().toString());
if (enabled) {
initialReconcile();
} else {
removeOccurrenceAnnotations();
}
}
}
@Override
public void initialReconcile() {
if (sourceViewer != null) {
sourceViewer.getTextWidget().getDisplay().asyncExec(() -> {
if (sourceViewer != null && sourceViewer.getTextWidget() != null) {
applyHighlights(sourceViewer.getSelectionProvider().getSelection());
}
});
}
}
void removeOccurrenceAnnotations() {
IAnnotationModel annotationModel = sourceViewer.getAnnotationModel();
if (annotationModel == null || fOccurrenceAnnotations == null) {
return;
}
synchronized (getLockObject(annotationModel)) {
if (annotationModel instanceof IAnnotationModelExtension) {
((IAnnotationModelExtension) annotationModel).replaceAnnotations(fOccurrenceAnnotations, null);
} else {
for (Annotation fOccurrenceAnnotation : fOccurrenceAnnotations) {
annotationModel.removeAnnotation(fOccurrenceAnnotation);
}
}
fOccurrenceAnnotations = null;
}
}
private static Object getLockObject(IAnnotationModel annotationModel) {
if (annotationModel instanceof ISynchronizable) {
Object lock = ((ISynchronizable) annotationModel).getLockObject();
if (lock != null) {
return lock;
}
}
return annotationModel;
}
@Override
public void setDocument(IDocument document) {
this.document = document;
}
@Override
public void reconcile(DirtyRegion dirtyRegion, IRegion subRegion) {
// Do nothing
}
@Override
public void reconcile(IRegion partition) {
// Do nothing
}
@Override
public void setProgressMonitor(IProgressMonitor monitor) {
// Not used
}
}