/*
* Copyright 2009-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.codehaus.groovy.eclipse.editor.highlighting;
import static org.codehaus.groovy.eclipse.editor.highlighting.HighlightedTypedPosition.HighlightKind.DEPRECATED;
import static org.codehaus.groovy.eclipse.editor.highlighting.HighlightedTypedPosition.HighlightKind.UNKNOWN;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import org.codehaus.groovy.eclipse.GroovyPlugin;
import org.codehaus.groovy.eclipse.core.GroovyCore;
import org.codehaus.groovy.eclipse.core.preferences.PreferenceConstants;
import org.codehaus.groovy.eclipse.editor.GroovyEditor;
import org.codehaus.jdt.groovy.model.GroovyCompilationUnit;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.groovy.core.util.ReflectionUtils;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor;
import org.eclipse.jdt.internal.ui.javaeditor.JavaSourceViewer;
import org.eclipse.jdt.internal.ui.javaeditor.SemanticHighlightingPresenter;
import org.eclipse.jdt.internal.ui.text.JavaPresentationReconciler;
import org.eclipse.jdt.internal.ui.text.java.IJavaReconcilingListener;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.PreferenceConverter;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.TextAttribute;
import org.eclipse.jface.text.TextPresentation;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.texteditor.ChainedPreferenceStore;
public class GroovySemanticReconciler implements IJavaReconcilingListener {
private static final String GROOVY_HIGHLIGHT_PREFERENCE = PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_GJDK_COLOR.replaceFirst("\\.color$", "");
private static final String STRING_HIGHLIGHT_PREFERENCE = PreferenceConstants.GROOVY_EDITOR_HIGHLIGHT_STRINGS_COLOR.replaceFirst("\\.color$", "");
private static final String NUMBER_HIGHLIGHT_PREFERENCE = "semanticHighlighting.number";
private static final String VARIABLE_HIGHLIGHT_PREFERENCE = "semanticHighlighting.localVariable";
private static final String PARAMETER_HIGHLIGHT_PREFERENCE = "semanticHighlighting.parameterVariable";
private static final String ANNOTATION_HIGHLIGHT_PREFERENCE = "semanticHighlighting.annotationElementReference";
private static final String DEPRECATED_HIGHLIGHT_PREFERENCE = "semanticHighlighting.deprecatedMember";
private static final String OBJECT_FIELD_HIGHLIGHT_PREFERENCE = "semanticHighlighting.field";
private static final String STATIC_FIELD_HIGHLIGHT_PREFERENCE = "semanticHighlighting.staticField";
private static final String STATIC_VALUE_HIGHLIGHT_PREFERENCE = "semanticHighlighting.staticFinalField";
private static final String OBJECT_METHOD_HIGHLIGHT_PREFERENCE = "semanticHighlighting.method";
private static final String STATIC_METHOD_HIGHLIGHT_PREFERENCE = "semanticHighlighting.staticMethodInvocation";
private static final String METHOD_DECLARATION_HIGHLIGHT_PREFERENCE = "semanticHighlighting.methodDeclarationName";
// these types have package-private visibility
private static Method GET_HIGHLIGHTING = null;
private static Constructor<?> HIGHLIGHTING_STYLE;
private static Constructor<?> HIGHLIGHTED_POSITION;
static {
try {
Class<?> style = Class.forName("org.eclipse.jdt.internal.ui.javaeditor.SemanticHighlightingManager$Highlighting");
HIGHLIGHTING_STYLE = ReflectionUtils.getConstructor(style, TextAttribute.class, boolean.class);
Class<?> position = Class.forName("org.eclipse.jdt.internal.ui.javaeditor.SemanticHighlightingManager$HighlightedPosition");
HIGHLIGHTED_POSITION = ReflectionUtils.getConstructor(position, int.class, int.class, style, Object.class);
GET_HIGHLIGHTING = position.getDeclaredMethod("getHighlighting");
GET_HIGHLIGHTING.setAccessible(true);
} catch (ClassNotFoundException cnfe) {
HIGHLIGHTING_STYLE = null;
HIGHLIGHTED_POSITION = null;
GroovyPlugin.getDefault().logError("Semantic highlighting disabled", cnfe);
} catch (NoSuchMethodException nsme) {
}
}
private volatile GroovyEditor editor;
private SemanticHighlightingPresenter presenter;
private final Semaphore lock = new Semaphore(1, true);
// make these configurable
private Object mapKeyHighlighting;
private Object tagKeyHighlighting;
private Object numberRefHighlighting;
private Object regexpRefHighlighting;
private Object undefinedRefHighlighting;
private Object deprecatedRefHighlighting;
private Object localHighlighting;
private Object paramHighlighting;
private Object objectFieldHighlighting;
private Object staticFieldHighlighting;
private Object staticValueHighlighting;
private Object methodDefHighlighting;
private Object methodUseHighlighting;
private Object groovyMethodUseHighlighting;
private Object staticMethodUseHighlighting;
public GroovySemanticReconciler() {
// TODO: Reload colors and styles when preferences are changed.
IPreferenceStore prefs = new ChainedPreferenceStore(new IPreferenceStore[] {
GroovyPlugin.getDefault().getPreferenceStore(), JavaPlugin.getDefault().getPreferenceStore()});
Color groovyColor = loadColorFrom(prefs, GROOVY_HIGHLIGHT_PREFERENCE);
Color numberColor = loadColorFrom(prefs, NUMBER_HIGHLIGHT_PREFERENCE);
Color stringColor = loadColorFrom(prefs, STRING_HIGHLIGHT_PREFERENCE);
Color tagKeyColor = loadColorFrom(prefs, ANNOTATION_HIGHLIGHT_PREFERENCE);
Color parameterColor = loadColorFrom(prefs, PARAMETER_HIGHLIGHT_PREFERENCE);
Color variableColor = loadColorFrom(prefs, VARIABLE_HIGHLIGHT_PREFERENCE);
Color objectFieldColor = loadColorFrom(prefs, OBJECT_FIELD_HIGHLIGHT_PREFERENCE);
Color staticFieldColor = loadColorFrom(prefs, STATIC_FIELD_HIGHLIGHT_PREFERENCE);
Color staticValueColor = loadColorFrom(prefs, STATIC_VALUE_HIGHLIGHT_PREFERENCE);
Color staticCallColor = loadColorFrom(prefs, STATIC_METHOD_HIGHLIGHT_PREFERENCE);
Color methodCallColor = loadColorFrom(prefs, OBJECT_METHOD_HIGHLIGHT_PREFERENCE);
Color methodDeclColor = loadColorFrom(prefs, METHOD_DECLARATION_HIGHLIGHT_PREFERENCE);
mapKeyHighlighting = newHighlightingStyle(stringColor);
tagKeyHighlighting = newHighlightingStyle(tagKeyColor, loadStyleFrom(prefs, ANNOTATION_HIGHLIGHT_PREFERENCE));
numberRefHighlighting = newHighlightingStyle(numberColor, loadStyleFrom(prefs, NUMBER_HIGHLIGHT_PREFERENCE));
regexpRefHighlighting = newHighlightingStyle(stringColor, SWT.ITALIC | loadStyleFrom(prefs, STRING_HIGHLIGHT_PREFERENCE));
deprecatedRefHighlighting = newHighlightingStyle(null, loadStyleFrom(prefs, DEPRECATED_HIGHLIGHT_PREFERENCE));
undefinedRefHighlighting = newHighlightingStyle(null, TextAttribute.UNDERLINE);
localHighlighting = newHighlightingStyle(variableColor, loadStyleFrom(prefs, VARIABLE_HIGHLIGHT_PREFERENCE));
paramHighlighting = newHighlightingStyle(parameterColor, loadStyleFrom(prefs, PARAMETER_HIGHLIGHT_PREFERENCE));
objectFieldHighlighting = newHighlightingStyle(objectFieldColor, loadStyleFrom(prefs, OBJECT_FIELD_HIGHLIGHT_PREFERENCE));
staticFieldHighlighting = newHighlightingStyle(staticFieldColor, loadStyleFrom(prefs, STATIC_FIELD_HIGHLIGHT_PREFERENCE));
staticValueHighlighting = newHighlightingStyle(staticValueColor, loadStyleFrom(prefs, STATIC_VALUE_HIGHLIGHT_PREFERENCE));
methodDefHighlighting = newHighlightingStyle(methodDeclColor, loadStyleFrom(prefs, METHOD_DECLARATION_HIGHLIGHT_PREFERENCE));
methodUseHighlighting = newHighlightingStyle(methodCallColor, loadStyleFrom(prefs, OBJECT_METHOD_HIGHLIGHT_PREFERENCE));
groovyMethodUseHighlighting = newHighlightingStyle(groovyColor, loadStyleFrom(prefs, GROOVY_HIGHLIGHT_PREFERENCE));
staticMethodUseHighlighting = newHighlightingStyle(staticCallColor, loadStyleFrom(prefs, STATIC_METHOD_HIGHLIGHT_PREFERENCE));
}
protected static Color loadColorFrom(IPreferenceStore prefs, String which) {
RGB color;
if (!prefs.contains(which + ".enabled") || prefs.getBoolean(which + ".enabled")) {
color = PreferenceConverter.getColor(prefs, which + ".color");
} else {
return null; // allow contextual default (i.e. string color)
//color = PreferenceConverter.getColor(prefs, "java_default");
//color = PreferenceConverter.getColor(GroovyPlugin.getDefault().getPreferenceStore(), PreferenceConstants.GROOVY_EDITOR_DEFAULT_COLOR);
}
return GroovyPlugin.getDefault().getTextTools().getColorManager().getColor(color);
}
protected static int loadStyleFrom(IPreferenceStore prefs, String which) {
int style = SWT.NONE;
if (!prefs.contains(which + ".enabled") || prefs.getBoolean(which + ".enabled")) {
if (prefs.getBoolean(which + ".bold") || prefs.getBoolean(which + ".color_bold"))
style |= SWT.BOLD;
if (prefs.getBoolean(which + ".italic"))
style |= SWT.ITALIC;
if (prefs.getBoolean(which + ".underline"))
style |= TextAttribute.UNDERLINE;
if (prefs.getBoolean(which + ".strikethrough"))
style |= TextAttribute.STRIKETHROUGH;
}
return style;
}
protected Object newHighlightingStyle(Color color) {
//return new HighlightingStyle(new TextAttribute(color), true);
return ReflectionUtils.invokeConstructor(HIGHLIGHTING_STYLE, new TextAttribute(color), Boolean.TRUE);
}
protected Object newHighlightingStyle(Color color, int style) {
//return new HighlightingStyle(new TextAttribute(color, null, style), true);
return ReflectionUtils.invokeConstructor(HIGHLIGHTING_STYLE, new TextAttribute(color, null, style), Boolean.TRUE);
}
public void install(GroovyEditor editor, JavaSourceViewer viewer) {
this.editor = editor;
presenter = new SemanticHighlightingPresenter();
presenter.install(viewer, (JavaPresentationReconciler) editor.getGroovyConfiguration().getPresentationReconciler(viewer));
}
public void uninstall() {
presenter.uninstall();
presenter = null;
editor = null;
}
public void aboutToBeReconciled() {
}
public void reconciled(CompilationUnit ast, boolean forced, IProgressMonitor monitor) {
if (ast != null && synchronize())
try {
if (editor == null) return; // uninstalled?
monitor.beginTask("Groovy semantic highlighting", 10);
GroovyCompilationUnit unit = editor.getGroovyCompilationUnit();
if (unit != null) {
presenter.setCanceled(monitor.isCanceled());
if (update(monitor, 1)) return;
GatherSemanticReferences finder = new GatherSemanticReferences(unit);
Collection<HighlightedTypedPosition> semanticReferences = finder.findSemanticHighlightingReferences();
if (update(monitor, 5)) return;
List<Position> newPositions = new ArrayList<Position>(semanticReferences.size());
List<Position> oldPositions = new LinkedList<Position>(getHighlightedPositions());
if (update(monitor, 1)) return;
HighlightedTypedPosition last = null; Position x = null;
for (HighlightedTypedPosition ref : semanticReferences) {
if (ref.compareTo(last) != 0) {
Position pos = newHighlightedPosition(ref);
x = tryAddPosition(newPositions, oldPositions, pos);
} else if (GET_HIGHLIGHTING != null && (ref.kind == DEPRECATED || ref.kind == UNKNOWN)) {
// this and last cover same source range and this indicates deprecated or unknown
Position pos = !newPositions.isEmpty() ? newPositions.get(newPositions.size() - 1) : null;
if (ref.compareTo(pos) != 0) {
if (ref.compareTo(x) == 0) {
pos = newHighlightedPosition(last);
newPositions.add(pos);
oldPositions.add(x);
} else {
GroovyPlugin.getDefault().logWarning(
String.format("Failed to apply %s semantic at %s",
ref.kind.name().toLowerCase(), ((Position) ref).toString()));
continue; // logic error?
}
}
Object style = GET_HIGHLIGHTING.invoke(pos);
TextAttribute one = getTextAttribute(style);
TextAttribute two = getTextAttribute(ref.kind == DEPRECATED ? deprecatedRefHighlighting : undefinedRefHighlighting);
// merge the text styling assigned to deprecated or unknown (usually strikethrough for deprecated and underline for unknown)
ReflectionUtils.setPrivateField(pos.getClass(), "fStyle", pos, newHighlightingStyle(one.getForeground(), one.getStyle() | two.getStyle()));
}
last = ref;
}
if (update(monitor, 2)) return;
TextPresentation textPresentation = null;
if (!presenter.isCanceled()) {
textPresentation = presenter.createPresentation(newPositions, oldPositions);
}
if (!presenter.isCanceled()) {
updatePresentation(textPresentation, newPositions, oldPositions);
}
update(monitor, 1);
}
} catch (Exception e) {
GroovyCore.logException("Semantic highlighting failed", e);
} finally {
lock.release();
monitor.done();
}
}
/**
* Ensures that only one thread at a time performs this task.
*/
private boolean synchronize() {
try {
boolean acquired = lock.tryAcquire(2, TimeUnit.SECONDS);
if (!acquired)
GroovyPlugin.getDefault().logWarning("Failed to acquire semantic highlight semaphore");
return acquired;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private boolean update(IProgressMonitor monitor, int units) {
monitor.worked(units);
return monitor.isCanceled();
}
@SuppressWarnings("unchecked")
private List<Position> getHighlightedPositions() {
// NOTE: Be very careful with this; fPositions is often accessed synchronously!
return (List<Position>) ReflectionUtils.getPrivateField(SemanticHighlightingPresenter.class, "fPositions", presenter);
}
private Position newHighlightedPosition(HighlightedTypedPosition pos) {
Object style = null;
switch (pos.kind) {
case DEPRECATED:
style = deprecatedRefHighlighting;
break;
case UNKNOWN:
style = undefinedRefHighlighting;
break;
case NUMBER:
style = numberRefHighlighting;
break;
case REGEXP:
style = regexpRefHighlighting;
break;
case MAP_KEY:
style = mapKeyHighlighting;
break;
case TAG_KEY:
style = tagKeyHighlighting;
break;
case VARIABLE:
style = localHighlighting;
break;
case PARAMETER:
style = paramHighlighting;
break;
case FIELD:
style = objectFieldHighlighting;
break;
case STATIC_FIELD:
style = staticFieldHighlighting;
break;
case STATIC_VALUE:
style = staticValueHighlighting;
break;
case CTOR:
case METHOD:
case STATIC_METHOD:
style = methodDefHighlighting;
break;
case CTOR_CALL:
case METHOD_CALL:
style = methodUseHighlighting;
break;
case GROOVY_CALL:
style = groovyMethodUseHighlighting;
break;
case STATIC_CALL:
style = staticMethodUseHighlighting;
break;
}
//return new HighlightedPosition(pos.offset, pos.length, style, this);
return (Position) ReflectionUtils.invokeConstructor(HIGHLIGHTED_POSITION, pos.offset, pos.length, style, this);
}
private Position tryAddPosition(List<Position> newPositions, List<Position> oldPositions, Position maybePosition) {
// TODO: Is there a quicker way to search for matches? These can be sorted easily.
for (Iterator<Position> it = oldPositions.iterator(); it.hasNext();) {
Position oldPosition = it.next();
if (!oldPosition.isDeleted() && oldPosition.equals(maybePosition) && isSameStyle(oldPosition, maybePosition)) {
it.remove(); // prevent old position from being removed from presentation
return oldPosition;
}
}
newPositions.add(maybePosition);
return null;
}
private boolean isSameStyle(Position a, Position b) {
if (GET_HIGHLIGHTING != null) {
try {
return (GET_HIGHLIGHTING.invoke(a) == GET_HIGHLIGHTING.invoke(b));
} catch (IllegalAccessException e) {
// fall through
} catch (IllegalArgumentException e) {
// fall through
} catch (InvocationTargetException e) {
// fall through
}
}
return true;
}
private TextAttribute getTextAttribute(Object highlightingStyle) {
// return highlightingStyle.getTextAttribute();
return (TextAttribute) ReflectionUtils.executeNoArgPrivateMethod(highlightingStyle.getClass(), "getTextAttribute", highlightingStyle);
}
/**
* Update the presentation.
*
* @param textPresentation the text presentation
* @param addedPositions the added positions
* @param removedPositions the removed positions
*/
private void updatePresentation(TextPresentation textPresentation, List<Position> addedPositions, List<Position> removedPositions) {
Runnable runnable = presenter.createUpdateRunnable(textPresentation, addedPositions, removedPositions);
if (runnable == null)
return;
JavaEditor thisEditor = editor;
if (thisEditor == null)
return;
IWorkbenchPartSite site = thisEditor.getSite();
if (site == null)
return;
Shell shell = site.getShell();
if (shell == null || shell.isDisposed())
return;
Display display = shell.getDisplay();
if (display == null || display.isDisposed())
return;
display.asyncExec(runnable);
}
}