blob: b218a82d9f8830270055401f33e74653f3b6f553 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2001, 2004 IBM Corporation 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:
* IBM Corporation - initial API and implementation
* Jens Lukowski/Innoopract - initial renaming/restructuring
*
*******************************************************************************/
package org.eclipse.wst.sse.ui.style;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.IDocumentExtension3;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.source.projection.ProjectionViewer;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.custom.LineStyleEvent;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.wst.sse.core.text.IStructuredDocument;
import org.eclipse.wst.sse.core.util.Debug;
import org.eclipse.wst.sse.ui.extension.ExtendedConfigurationBuilder;
import org.eclipse.wst.sse.ui.internal.Logger;
import org.eclipse.wst.sse.ui.internal.SSEUIPlugin;
import org.eclipse.wst.sse.ui.internal.preferences.EditorPreferenceNames;
import org.eclipse.wst.sse.ui.util.EditorUtility;
/**
* This class is to directly mediate between the Structured Document data
* structure and the text widget's text and events. It assumes there only the
* model is interested in text events, and all other views will work from that
* model. Changes to the text widgets input can cause changes in the model,
* which in turn cause changes to the widget's display.
*
*/
public class Highlighter implements IHighlighter {
private final boolean DEBUG = false;
private final StyleRange[] EMPTY_STYLE_RANGE = new StyleRange[0];
private static final String LINE_STYLE_PROVIDER_EXTENDED_ID = "linestyleprovider";
private IPropertyChangeListener fForegroundScaleListener = new IPropertyChangeListener() {
public void propertyChange(PropertyChangeEvent event) {
if (EditorPreferenceNames.READ_ONLY_FOREGROUND_SCALE.equals(event.getProperty())) {
IPreferenceStore editorStore = SSEUIPlugin.getDefault().getPreferenceStore();
readOnlyForegroundScaleFactor = editorStore.getInt(EditorPreferenceNames.READ_ONLY_FOREGROUND_SCALE);
disposeColorTable();
refreshDisplay();
}
}
};
private List fHoldStyleResults;
private String fPartitioning = IDocumentExtension3.DEFAULT_PARTITIONING;
private int fSavedLength = -1;
private int fSavedOffset = -1;
private StyleRange[] fSavedRanges = null;
private IStructuredDocument fStructuredDocument;
private Map fTableOfProviders;
private Map fExtendedProviders;
protected final LineStyleProvider NOOP_PROVIDER = new LineStyleProviderForNoOp();
private double readOnlyBackgroundScaleFactor = 10;
private Hashtable readOnlyColorTable;
double readOnlyForegroundScaleFactor = 30;
private YUV_RGBConverter rgbConverter;
private ITextViewer textViewer;
private StyledText textWidget;
public Highlighter() {
super();
}
protected void addEmptyRange(int start, int length, Collection holdResults) {
StyleRange result = new StyleRange();
result.start = start;
result.length = length;
holdResults.add(result);
}
public void addProvider(String partitionType, LineStyleProvider provider) {
getTableOfProviders().put(partitionType, provider);
}
/**
* Adjust the style ranges' start and length so that they refer to the
* textviewer widget's range instead of the textviewer's document range.
*
* @param ranges
* @param adjustment
*/
protected void adjust(StyleRange[] ranges, int adjustment) {
ITextViewer viewer = getTextViewer();
ITextViewerExtension5 extension = null;
if (viewer instanceof ITextViewerExtension5) {
extension = (ITextViewerExtension5) viewer;
}
// convert document regions back to widget regions
for (int i = 0; i < ranges.length; i++) {
if (extension != null) {
// get document range, taking into account folding regions in
// viewer
IRegion region = extension.modelRange2WidgetRange(new Region(
ranges[i].start, ranges[i].length));
if (region != null) {
ranges[i].start = region.getOffset();
ranges[i].length = region.getLength();
} // else what happens if region is not found?!
} else {
// just adjust the range using the given adjustment
ranges[i].start += adjustment;
}
}
}
/**
* @deprecated - Read Only areas have unchanged background colors
*/
void adjustBackground(StyleRange styleRange) {
RGB oldRGB = null;
Color oldColor = styleRange.background;
if (oldColor == null) {
oldColor = getTextWidget().getBackground();
}
oldRGB = oldColor.getRGB();
Color newColor = getCachedColorFor(oldRGB);
if (newColor == null) {
double target = getRGBConverter().calculateYComponent(oldColor);
// if background is "light" make it darker, and vice versa
if (target < 0.5)
target = 1.0;
else
target = 0.0;
RGB newRGB = getRGBConverter().transformRGB(oldRGB, readOnlyBackgroundScaleFactor / 100.0, target);
cacheColor(oldRGB, newRGB);
newColor = getCachedColorFor(oldRGB);
}
styleRange.background = newColor;
}
private void adjustForeground(StyleRange styleRange) {
RGB oldRGB = null;
// Color oldColor = styleRange.foreground;
Color oldColor = styleRange.background;
if (oldColor == null) {
// oldRGB = getTextWidget().getForeground().getRGB();
oldColor = getTextWidget().getBackground();
oldRGB = oldColor.getRGB();
}
else {
oldRGB = oldColor.getRGB();
}
Color newColor = getCachedColorFor(oldRGB);
if (newColor == null) {
// make text "closer to" background lumanence
double target = getRGBConverter().calculateYComponent(oldColor);
RGB newRGB = getRGBConverter().transformRGBToGrey(oldRGB, readOnlyForegroundScaleFactor / 100.0, target);
// save conversion, so calculations only need to be done once
cacheColor(oldRGB, newRGB);
newColor = getCachedColorFor(oldRGB);
}
styleRange.foreground = newColor;
}
/**
* Cache read-only color.
*
* @param oldRGB
* @param newColor
*/
private void cacheColor(RGB oldRGB, RGB newColor) {
if (readOnlyColorTable == null) {
readOnlyColorTable = new Hashtable();
}
readOnlyColorTable.put(oldRGB, newColor);
}
/**
* @param result
* @return
*/
private StyleRange[] convertReadOnlyRegions(StyleRange[] result, int start, int length) {
IStructuredDocument structuredDocument = getDocument();
/**
* (dmw) For client/provider simplicity (and consistent look and feel)
* we'll handle readonly regions in one spot, here in the Highlighter.
* Currently it is a fair assumption that each readonly region will be
* on an ITextRegion boundary, so we combine consecutive styles when
* found to be equivalent. Plus, for now, we'll just adjust
* foreground. Eventually will use a "dimming" algrorithm to adjust
* color's satuation/brightness.
*/
if (structuredDocument.containsReadOnly(start, length)) {
// something is read-only in the line, so go through each style,
// and adjust
for (int i = 0; i < result.length; i++) {
StyleRange styleRange = result[i];
if (structuredDocument.containsReadOnly(styleRange.start, styleRange.length)) {
adjustForeground(styleRange);
}
}
}
return result;
}
/**
* Clear out the readOnlyColorTable
*/
void disposeColorTable() {
if (readOnlyColorTable != null) {
readOnlyColorTable.clear();
}
readOnlyColorTable = null;
}
/**
* This method is just to get existing read-only colors.
*/
private Color getCachedColorFor(RGB oldRGB) {
Color result = null;
if (readOnlyColorTable != null) {
RGB readOnlyRGB = (RGB) readOnlyColorTable.get(oldRGB);
result = EditorUtility.getColor(readOnlyRGB);
}
return result;
}
protected IStructuredDocument getDocument() {
return fStructuredDocument;
}
/**
* Adjust the given widget offset and length so that they are the textviewer
* document's offset and length, taking into account what is actually
* visible in the document.
*
* @param offset
* @param length
* @return a region containing the offset and length within the textviewer's
* document or null if the offset is not within the document
*/
private IRegion getDocumentRangeFromWidgetRange(int offset, int length) {
IRegion styleRegion = null;
ITextViewer viewer = getTextViewer();
if (viewer instanceof ITextViewerExtension5) {
// get document range, taking into account folding regions in viewer
ITextViewerExtension5 extension = (ITextViewerExtension5) viewer;
styleRegion = extension.widgetRange2ModelRange(new Region(offset,
length));
} else {
// get document range, taking into account viewer visible region
// get visible region in viewer
IRegion vr = null;
if (viewer != null)
vr = viewer.getVisibleRegion();
else
vr = new Region(0, getDocument().getLength());
// if offset is not within visible region, then we don't really care
if (offset <= vr.getLength()) {
// Adjust the offset to be within visible region
styleRegion = new Region(offset + vr.getOffset(), length);
}
}
return styleRegion;
}
private Map getExtendedProviders() {
if (fExtendedProviders == null) {
fExtendedProviders = new HashMap(3);
}
return fExtendedProviders;
}
/**
* Method getProviderFor.
*
* @param typedRegion
* @return LineStyleProvider
*/
private LineStyleProvider getProviderFor(ITypedRegion typedRegion) {
String type = typedRegion.getType();
LineStyleProvider result = (LineStyleProvider) fTableOfProviders.get(type);
if (result == null) {
// NOT YET FINALIZED - DO NOT CONSIDER AS API
synchronized (getExtendedProviders()) {
if (!getExtendedProviders().containsKey(type)) {
LineStyleProvider provider = (LineStyleProvider) ExtendedConfigurationBuilder.getInstance().getConfiguration(LINE_STYLE_PROVIDER_EXTENDED_ID, type);
getExtendedProviders().put(type, provider);
if (provider != null) {
provider.init(getDocument(), this);
}
result = provider;
}
else {
result = (LineStyleProvider) getExtendedProviders().get(type);
}
}
}
if (result == null) {
result = NOOP_PROVIDER;
}
return result;
}
private YUV_RGBConverter getRGBConverter() {
if (rgbConverter == null) {
rgbConverter = new YUV_RGBConverter();
}
return rgbConverter;
}
private Map getTableOfProviders() {
if (fTableOfProviders == null) {
fTableOfProviders = new HashMap();
}
return fTableOfProviders;
}
/**
* Returns the textViewer.
*
* @return ITextViewer
*/
public ITextViewer getTextViewer() {
return textViewer;
}
/**
* @return
*/
protected StyledText getTextWidget() {
return textWidget;
}
public void install(ITextViewer newTextViewer) {
this.textViewer = newTextViewer;
IPreferenceStore editorStore = SSEUIPlugin.getDefault().getPreferenceStore();
editorStore.addPropertyChangeListener(fForegroundScaleListener);
readOnlyForegroundScaleFactor = editorStore.getInt(EditorPreferenceNames.READ_ONLY_FOREGROUND_SCALE);
if (textWidget != null) {
textWidget.removeLineStyleListener(this);
}
textWidget = newTextViewer.getTextWidget();
if (textWidget != null) {
textWidget.addLineStyleListener(this);
}
refreshDisplay();
}
public StyleRange[] lineGetStyle(int eventLineOffset, int eventLineLength) {
StyleRange[] eventStyles = EMPTY_STYLE_RANGE;
try {
if (getDocument() == null || eventLineLength == 0) {
// getDocument() == null
// during initialization, this is sometimes called before our
// structured
// is set, in which case we set styles to be the empty style
// range
// (event.styles can not be null)
// eventLineLength == 0
// we sometimes get odd requests from the very last CRLF in the
// document
// it has no length, and there is no node for it!
eventStyles = EMPTY_STYLE_RANGE;
} else {
/*
* LineStyleProviders work using absolute document
* offsets. To support visible regions, adjust the
* requested range up to the full document offsets.
*/
IRegion styleRegion = getDocumentRangeFromWidgetRange(
eventLineOffset, eventLineLength);
if (styleRegion != null) {
int start = styleRegion.getOffset();
int length = styleRegion.getLength();
ITypedRegion[] partitions = TextUtilities.computePartitioning(getDocument(), fPartitioning, start, length, false);
eventStyles = prepareStyleRangesArray(partitions, start, length);
// If there is a subtext offset, the style ranges must be
// adjusted to the expected
// offsets
// just check if eventLineOffset is different than start
// then adjust, otherwise u can leave it alone
// unless there is special handling for
// itextviewerextension5?
if (start != eventLineOffset) {
int offset = 0;
// figure out visible region to use for adjustment
// only adjust if need to
if (!(getTextViewer() instanceof ITextViewerExtension5)) {
IRegion vr = getTextViewer().getVisibleRegion();
if (vr != null) {
offset = vr.getOffset();
if (offset > 0)
adjust(eventStyles, -offset);
}
} else {
if (getTextViewer() instanceof ProjectionViewer) {
if (((ProjectionViewer)getTextViewer()).isProjectionMode())
adjust(eventStyles, -offset);
}
}
}
// for debugging only
if (DEBUG) {
if (!valid(eventStyles, eventLineOffset, eventLineLength)) {
Logger.log(Logger.WARNING, "Highlighter::lineGetStyle found invalid styles at offset " + eventLineOffset); //$NON-NLS-1$
}
}
}
}
}
catch (Exception e) {
// if ANY exception occurs during highlighting,
// just return "no highlighting"
eventStyles = EMPTY_STYLE_RANGE;
if (Debug.syntaxHighlighting) {
System.out.println("Exception during highlighting!"); //$NON-NLS-1$
}
}
return eventStyles;
}
/**
* A passthrough method that extracts relevant data from the
* LineStyleEvent and passes it along. This method was separated for
* performance testing purposes.
*
* @see org.eclipse.swt.custom.LineStyleListener#lineGetStyle(LineStyleEvent)
*/
public void lineGetStyle(LineStyleEvent event) {
int offset = event.lineOffset;
int length = event.lineText.length();
/*
* For some reason, we are sometimes asked for the same style range
* over and over again. This was found to happen during 'revert' of a
* file with one line in it that is 40K long! So, while we don't know
* root cause, caching the styled ranges in case the exact same
* request is made multiple times seems like cheap insurance.
*/
if (offset == fSavedOffset && length == fSavedLength && fSavedRanges != null) {
event.styles = fSavedRanges;
}
else {
// need to assign this array here, or else the field won't get
// updated
event.styles = lineGetStyle(offset, length);
// now saved "cached data" for repeated requests which are exaclty
// same
fSavedOffset = offset;
fSavedLength = length;
fSavedRanges = event.styles;
}
}
/**
* Note: its very important this method never return null, which is why
* the final null check is in a finally clause
*/
protected StyleRange[] prepareStyleRangesArray(ITypedRegion[] partitions, int start, int length) {
StyleRange[] result = EMPTY_STYLE_RANGE;
if (fHoldStyleResults == null) {
fHoldStyleResults = new ArrayList(partitions.length);
}
else {
fHoldStyleResults.clear();
}
// TODO: make some of these instance variables to prevent creation on
// stack
LineStyleProvider currentLineStyleProvider = null;
boolean handled = false;
for (int i = 0; i < partitions.length; i++) {
ITypedRegion currentPartition = partitions[i];
currentLineStyleProvider = getProviderFor(currentPartition);
currentLineStyleProvider.init(getDocument(), this);
handled = currentLineStyleProvider.prepareRegions(currentPartition, currentPartition.getOffset(), currentPartition.getLength(), fHoldStyleResults);
if (Debug.syntaxHighlighting && !handled) {
System.out.println("Did not handle highlighting in Highlighter inner while"); //$NON-NLS-1$
}
}
int resultSize = fHoldStyleResults.size();
if (resultSize > 0) {
result = (StyleRange[]) fHoldStyleResults.toArray(new StyleRange[fHoldStyleResults.size()]);
}
else {
result = EMPTY_STYLE_RANGE;
}
result = convertReadOnlyRegions(result, start, length);
return result;
}
public void refreshDisplay() {
if (textWidget != null && !textWidget.isDisposed())
textWidget.redraw();
}
/**
*/
public void refreshDisplay(int start, int length) {
if (textWidget != null && !textWidget.isDisposed())
textWidget.redrawRange(start, length, true);
}
public void removeProvider(String partitionType) {
getTableOfProviders().remove(partitionType);
}
public void setDocument(IStructuredDocument structuredDocument) {
fStructuredDocument = structuredDocument;
}
public void setDocumentPartitioning(String partitioning) {
if (partitioning != null) {
fPartitioning = partitioning;
}
else {
fPartitioning = IDocumentExtension3.DEFAULT_PARTITIONING;
}
}
/*
* (non-Javadoc)
*
* @see com.ibm.sed.structured.style.IHighlighter#uninstall()
*/
public void uninstall() {
if (textWidget != null && !textWidget.isDisposed()) {
textWidget.removeLineStyleListener(this);
}
textWidget = null;
Collection providers = getTableOfProviders().values();
Iterator iterator = providers.iterator();
while (iterator.hasNext()) {
LineStyleProvider lineStyleProvider = (LineStyleProvider) iterator.next();
lineStyleProvider.release();
// this remove probably isn't strictly needed, since
// typically highlighter instance as a whole will go
// away ... but in case that ever changes, this seems like
// a better style.
iterator.remove();
}
synchronized (getExtendedProviders()) {
providers = new ArrayList(getExtendedProviders().values());
getExtendedProviders().clear();
}
iterator = providers.iterator();
while (iterator.hasNext()) {
LineStyleProvider lineStyleProvider = (LineStyleProvider) iterator.next();
if (lineStyleProvider != null) {
lineStyleProvider.release();
iterator.remove();
}
}
IPreferenceStore editorStore = SSEUIPlugin.getDefault().getPreferenceStore();
editorStore.removePropertyChangeListener(fForegroundScaleListener);
disposeColorTable();
// clear out cached variables (d282894)
fSavedOffset = -1;
fSavedLength = -1;
fSavedRanges = null;
}
/**
* Purely a debugging aide.
*/
private boolean valid(StyleRange[] eventStyles, int startOffset, int lineLength) {
boolean result = false;
if (eventStyles != null) {
if (eventStyles.length > 0) {
StyleRange first = eventStyles[0];
StyleRange last = eventStyles[eventStyles.length - 1];
if (startOffset > first.start) {
result = false;
}
else {
int lineEndOffset = startOffset + lineLength;
int lastOffset = last.start + last.length;
if (lastOffset > lineEndOffset) {
result = false;
}
else {
result = true;
}
}
}
else {
// a zero length array is ok
result = true;
}
}
return result;
}
}