blob: cf06ee9a85e80459de42b946810ddf540f28f4f1 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2017 IBM Corporation and others.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.dltk.internal.ui.editor.semantic.highlighting;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.eclipse.dltk.internal.ui.editor.ScriptSourceViewer;
import org.eclipse.dltk.ui.DLTKUIPlugin;
import org.eclipse.dltk.ui.editor.highlighting.HighlightedPosition;
import org.eclipse.dltk.ui.editor.highlighting.HighlightingStyle;
import org.eclipse.dltk.ui.editor.highlighting.IHighlightedPositionFactory;
import org.eclipse.dltk.ui.text.ScriptPresentationReconciler;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.IPositionUpdater;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ISynchronizable;
import org.eclipse.jface.text.ITextInputListener;
import org.eclipse.jface.text.ITextPresentationListener;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextPresentation;
import org.eclipse.swt.custom.StyleRange;
/**
* Semantic highlighting presenter - UI thread implementation.
*
* @since 3.0
*/
public class SemanticHighlightingPresenter implements ITextPresentationListener,
ITextInputListener, IDocumentListener, IHighlightedPositionFactory {
/**
* Semantic highlighting position updater.
*/
private class HighlightingPositionUpdater implements IPositionUpdater {
/** The position category. */
private final String fCategory;
/**
* Creates a new updater for the given <code>category</code>.
*
* @param category
* the new category.
*/
public HighlightingPositionUpdater(String category) {
fCategory = category;
}
/*
* @see IPositionUpdater#update(DocumentEvent)
*/
@Override
public void update(DocumentEvent event) {
int eventOffset = event.getOffset();
int eventOldLength = event.getLength();
int eventEnd = eventOffset + eventOldLength;
try {
Position[] positions = event.getDocument()
.getPositions(fCategory);
for (int i = 0; i != positions.length; i++) {
HighlightedPosition position = (HighlightedPosition) positions[i];
// Also update deleted positions because they get deleted by
// the background thread and removed/invalidated only in the
// UI runnable
// if (position.isDeleted())
// continue;
int offset = position.getOffset();
int length = position.getLength();
int end = offset + length;
if (offset > eventEnd)
updateWithPrecedingEvent(position, event);
else if (end < eventOffset)
updateWithSucceedingEvent(position, event);
else if (offset <= eventOffset && end >= eventEnd)
updateWithIncludedEvent(position, event);
else if (offset <= eventOffset)
updateWithOverEndEvent(position, event);
else if (end >= eventEnd)
updateWithOverStartEvent(position, event);
else
updateWithIncludingEvent(position, event);
}
} catch (BadPositionCategoryException e) {
// ignore and return
}
}
/**
* Update the given position with the given event. The event precedes
* the position.
*
* @param position
* The position
* @param event
* The event
*/
private void updateWithPrecedingEvent(HighlightedPosition position,
DocumentEvent event) {
String newText = event.getText();
int eventNewLength = newText != null ? newText.length() : 0;
int deltaLength = eventNewLength - event.getLength();
position.setOffset(position.getOffset() + deltaLength);
}
/**
* Update the given position with the given event. The event succeeds
* the position.
*
* @param position
* The position
* @param event
* The event
*/
private void updateWithSucceedingEvent(HighlightedPosition position,
DocumentEvent event) {
}
/**
* Update the given position with the given event. The event is included
* by the position.
*
* @param position
* The position
* @param event
* The event
*/
private void updateWithIncludedEvent(HighlightedPosition position,
DocumentEvent event) {
int eventOffset = event.getOffset();
String newText = event.getText();
if (newText == null)
newText = ""; //$NON-NLS-1$
int eventNewLength = newText.length();
int deltaLength = eventNewLength - event.getLength();
int offset = position.getOffset();
int length = position.getLength();
int end = offset + length;
int includedLength = 0;
while (includedLength < eventNewLength && Character
.isJavaIdentifierPart(newText.charAt(includedLength)))
includedLength++;
if (includedLength == eventNewLength)
position.setLength(length + deltaLength);
else {
int newLeftLength = eventOffset - offset + includedLength;
int excludedLength = eventNewLength;
while (excludedLength > 0 && Character.isJavaIdentifierPart(
newText.charAt(excludedLength - 1)))
excludedLength--;
int newRightOffset = eventOffset + excludedLength;
int newRightLength = end + deltaLength - newRightOffset;
if (newRightLength == 0) {
position.setLength(newLeftLength);
} else {
if (newLeftLength == 0) {
position.update(newRightOffset, newRightLength);
} else {
position.setLength(newLeftLength);
// addPositionFromUI(newRightOffset, newRightLength,
// position.getHighlighting());
}
}
}
}
/**
* Update the given position with the given event. The event overlaps
* with the end of the position.
*
* @param position
* The position
* @param event
* The event
*/
private void updateWithOverEndEvent(HighlightedPosition position,
DocumentEvent event) {
String newText = event.getText();
if (newText == null)
newText = ""; //$NON-NLS-1$
int eventNewLength = newText.length();
int includedLength = 0;
while (includedLength < eventNewLength && Character
.isJavaIdentifierPart(newText.charAt(includedLength)))
includedLength++;
position.setLength(
event.getOffset() - position.getOffset() + includedLength);
}
/**
* Update the given position with the given event. The event overlaps
* with the start of the position.
*
* @param position
* The position
* @param event
* The event
*/
private void updateWithOverStartEvent(HighlightedPosition position,
DocumentEvent event) {
int eventOffset = event.getOffset();
int eventEnd = eventOffset + event.getLength();
String newText = event.getText();
if (newText == null)
newText = ""; //$NON-NLS-1$
int eventNewLength = newText.length();
int excludedLength = eventNewLength;
while (excludedLength > 0 && Character
.isJavaIdentifierPart(newText.charAt(excludedLength - 1)))
excludedLength--;
int deleted = eventEnd - position.getOffset();
int inserted = eventNewLength - excludedLength;
position.update(eventOffset + excludedLength,
position.getLength() - deleted + inserted);
}
/**
* Update the given position with the given event. The event includes
* the position.
*
* @param position
* The position
* @param event
* The event
*/
private void updateWithIncludingEvent(HighlightedPosition position,
DocumentEvent event) {
position.delete();
position.update(event.getOffset(), 0);
}
}
/** Position updater */
private IPositionUpdater fPositionUpdater = new HighlightingPositionUpdater(
getPositionCategory());
/**
* The source viewer this semantic highlighting reconciler is installed on
*/
private ScriptSourceViewer fSourceViewer;
/** The background presentation reconciler */
private ScriptPresentationReconciler fPresentationReconciler;
/**
* UI's current highlighted positions - can contain <code>null</code>
* elements
*/
private List<HighlightedPosition> fPositions = new ArrayList<>();
/** UI position lock */
private Object fPositionLock = new Object();
/** <code>true</code> iff the current reconcile is canceled. */
private boolean fIsCanceled = false;
/**
* Creates and returns a new highlighted position with the given offset,
* length and highlighting.
* <p>
* NOTE: Also called from background thread.
* </p>
*
* @param offset
* The offset
* @param length
* The length
* @param highlighting
* The highlighting
* @return The new highlighted position
*/
@Override
public HighlightedPosition createHighlightedPosition(int offset, int length,
HighlightingStyle highlighting) {
// TODO: reuse deleted positions
return new HighlightedPosition(offset, length, highlighting,
fPositionUpdater);
}
/**
* Adds all current positions to the given list.
* <p>
* NOTE: Called from background thread.
* </p>
*
* @param list
* The list
*/
public void addAllPositions(List<HighlightedPosition> list) {
synchronized (fPositionLock) {
list.addAll(fPositions);
}
}
/**
* Returns all current positions.
* <p>
* NOTE: Called from background thread.
* </p>
*/
public HighlightedPosition[] getPositions() {
synchronized (fPositionLock) {
final HighlightedPosition[] result = new HighlightedPosition[fPositions
.size()];
fPositions.toArray(result);
return result;
}
}
/**
* Create a text presentation in the background.
* <p>
* NOTE: Called from background thread.
* </p>
*
* @param addedPositions
* the added positions
* @param removedPositions
* the removed positions
* @return the text presentation or <code>null</code>, if reconciliation
* should be canceled
*/
public TextPresentation createPresentation(
HighlightedPosition[] addedPositions,
HighlightedPosition[] removedPositions) {
ScriptSourceViewer sourceViewer = fSourceViewer;
ScriptPresentationReconciler presentationReconciler = fPresentationReconciler;
if (sourceViewer == null || presentationReconciler == null)
return null;
if (isCanceled())
return null;
IDocument document = sourceViewer.getDocument();
if (document == null)
return null;
int minStart = Integer.MAX_VALUE;
int maxEnd = Integer.MIN_VALUE;
for (int i = 0, n = removedPositions.length; i < n; i++) {
Position position = removedPositions[i];
int offset = position.getOffset();
minStart = Math.min(minStart, offset);
maxEnd = Math.max(maxEnd, offset + position.getLength());
}
for (int i = 0, n = addedPositions.length; i < n; i++) {
Position position = addedPositions[i];
int offset = position.getOffset();
minStart = Math.min(minStart, offset);
maxEnd = Math.max(maxEnd, offset + position.getLength());
}
if (minStart < maxEnd)
try {
return presentationReconciler.createRepairDescription(
new Region(minStart, maxEnd - minStart), document);
} catch (RuntimeException e) {
// Assume concurrent modification from UI thread
}
return null;
}
/**
* Create a runnable for updating the presentation.
* <p>
* NOTE: Called from background thread.
* </p>
*
* @param textPresentation
* the text presentation
* @param added
* the added positions
* @param removed
* the removed positions
* @return the runnable or <code>null</code>, if reconciliation should be
* canceled
*/
public Runnable createUpdateRunnable(
final TextPresentation textPresentation,
final HighlightedPosition[] added,
final HighlightedPosition[] removed) {
if (fSourceViewer == null || textPresentation == null)
return null;
// TODO: do clustering of positions and post multiple fast runnables
if (isCanceled())
return null;
Runnable runnable = () -> updatePresentation(textPresentation, added,
removed);
return runnable;
}
/**
* Invalidate the presentation of the positions based on the given added
* positions and the existing deleted positions. Also unregisters the
* deleted positions from the document and patches the positions of this
* presenter.
* <p>
* NOTE: Indirectly called from background thread by UI runnable.
* </p>
*
* @param textPresentation
* the text presentation or <code>null</code>, if the
* presentation should computed in the UI thread
* @param addedPositions
* the added positions
* @param removedPositions
* the removed positions
*/
public void updatePresentation(TextPresentation textPresentation,
HighlightedPosition[] addedPositions,
HighlightedPosition[] removedPositions) {
if (fSourceViewer == null)
return;
// checkOrdering("added positions: ", Arrays.asList(addedPositions));
// //$NON-NLS-1$
// checkOrdering("removed positions: ",
// Arrays.asList(removedPositions)); //$NON-NLS-1$
// checkOrdering("old positions: ", fPositions); //$NON-NLS-1$
// TODO: double-check consistency with document.getPositions(...)
// TODO: reuse removed positions
if (isCanceled())
return;
IDocument document = fSourceViewer.getDocument();
if (document == null)
return;
String positionCategory = getPositionCategory();
List<HighlightedPosition> removedPositionsList = Arrays
.asList(removedPositions);
try {
synchronized (fPositionLock) {
List<HighlightedPosition> oldPositions = fPositions;
int newSize = Math.max(fPositions.size() + addedPositions.length
- removedPositions.length, 10);
/*
* The following loop is a kind of merge sort: it merges two
* List<Position>, each sorted by position.offset, into one new
* list. The first of the two is the previous list of positions
* (oldPositions), from which any deleted positions get removed
* on the fly. The second of two is the list of added positions.
* The result is stored in newPositions.
*/
List<HighlightedPosition> newPositions = new ArrayList<>(
newSize);
HighlightedPosition position = null;
HighlightedPosition addedPosition = null;
for (int i = 0, j = 0, n = oldPositions
.size(), m = addedPositions.length; i < n
|| position != null || j < m
|| addedPosition != null;) {
// loop variant: i + j < old(i + j)
// a) find the next non-deleted Position from the old list
while (position == null && i < n) {
position = oldPositions.get(i++);
if (position.isDeleted()
|| contain(removedPositionsList, position)) {
document.removePosition(positionCategory, position);
position = null;
}
}
// b) find the next Position from the added list
if (addedPosition == null && j < m) {
addedPosition = addedPositions[j++];
document.addPosition(positionCategory, addedPosition);
}
// c) merge: add the next of position/addedPosition with the
// lower offset
if (position != null) {
if (addedPosition != null)
if (position.getOffset() <= addedPosition
.getOffset()) {
newPositions.add(position);
position = null;
} else {
newPositions.add(addedPosition);
addedPosition = null;
}
else {
newPositions.add(position);
position = null;
}
} else if (addedPosition != null) {
newPositions.add(addedPosition);
addedPosition = null;
}
}
// if (newPositions.size()>2){
// System.out.println("A");
// }
fPositions = newPositions;
}
} catch (BadPositionCategoryException e) {
// Should not happen
DLTKUIPlugin.log(e);
} catch (BadLocationException e) {
// Should not happen
DLTKUIPlugin.log(e);
}
// checkOrdering("new positions: ", fPositions); //$NON-NLS-1$
if (textPresentation != null)
fSourceViewer.changeTextPresentation(textPresentation, false);
else
fSourceViewer.invalidateTextPresentation();
}
// private void checkOrdering(String s, List positions) {
// Position previous= null;
// for (int i= 0, n= positions.size(); i < n; i++) {
// Position current= (Position) positions.get(i);
// if (previous != null && previous.getOffset() + previous.getLength() >
// current.getOffset())
// return;
// }
// }
/**
* Returns <code>true</code> iff the positions contain the position.
*
* @param positions
* the positions, must be ordered by offset but may overlap
* @param position
* the position
* @return <code>true</code> iff the positions contain the position
*/
private boolean contain(List<? extends Position> positions,
Position position) {
return indexOf(positions, position) != -1;
}
/**
* Returns index of the position in the positions, <code>-1</code> if not
* found.
*
* @param positions
* the positions, must be ordered by offset but may overlap
* @param position
* the position
* @return the index
*/
private int indexOf(List<? extends Position> positions, Position position) {
int index = computeIndexAtOffset(positions, position.getOffset());
int size = positions.size();
while (index < size) {
if (positions.get(index) == position)
return index;
index++;
}
return -1;
}
/**
* Returns the index of the first position with an offset equal or greater
* than the given offset.
*
* @param positions
* the positions, must be ordered by offset and must not overlap
* @param offset
* the offset
* @return the index of the last position with an offset equal or greater
* than the given offset
*/
private int computeIndexAtOffset(List<? extends Position> positions,
int offset) {
int i = -1;
int j = positions.size();
while (j - i > 1) {
int k = (i + j) >> 1;
Position position = positions.get(k);
if (position.getOffset() >= offset)
j = k;
else
i = k;
}
return j;
}
/*
* @see ITextPresentationListener#applyTextPresentation (TextPresentation)
*/
@Override
public void applyTextPresentation(TextPresentation textPresentation) {
IRegion region = textPresentation.getExtent();
int i = computeIndexAtOffset(fPositions, region.getOffset()),
n = computeIndexAtOffset(fPositions,
region.getOffset() + region.getLength());
if (n - i > 2) {
List<StyleRange> ranges = new ArrayList<>(n - i);
for (; i < n; i++) {
HighlightedPosition position = fPositions.get(i);
if (!position.isDeleted())
ranges.add(position.createStyleRange());
}
StyleRange[] array = new StyleRange[ranges.size()];
array = ranges.toArray(array);
textPresentation.replaceStyleRanges(array);
} else {
for (; i < n; i++) {
HighlightedPosition position = fPositions.get(i);
if (!position.isDeleted())
textPresentation
.replaceStyleRange(position.createStyleRange());
}
}
}
/*
* @see
* ITextInputListener#inputDocumentAboutToBeChanged(IDocument,IDocument)
*/
@Override
public void inputDocumentAboutToBeChanged(IDocument oldInput,
IDocument newInput) {
setCanceled(true);
releaseDocument(oldInput);
resetState();
}
/*
* @see ITextInputListener#inputDocumentChanged(IDocument, IDocument)
*/
@Override
public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
manageDocument(newInput);
}
/*
* @see IDocumentListener#documentAboutToBeChanged(DocumentEvent)
*/
@Override
public void documentAboutToBeChanged(DocumentEvent event) {
setCanceled(true);
}
/*
* @see IDocumentListener#documentChanged(DocumentEvent)
*/
@Override
public void documentChanged(DocumentEvent event) {
}
/**
* @return Returns <code>true</code> iff the current reconcile is canceled.
* <p>
* NOTE: Also called from background thread.
* </p>
*/
public boolean isCanceled() {
IDocument document = fSourceViewer != null ? fSourceViewer.getDocument()
: null;
if (document == null)
return fIsCanceled;
synchronized (getLockObject(document)) {
return fIsCanceled;
}
}
/**
* Set whether or not the current reconcile is canceled.
*
* @param isCanceled
* <code>true</code> iff the current reconcile is canceled
*/
public void setCanceled(boolean isCanceled) {
IDocument document = fSourceViewer != null ? fSourceViewer.getDocument()
: null;
if (document == null) {
fIsCanceled = isCanceled;
return;
}
synchronized (getLockObject(document)) {
fIsCanceled = isCanceled;
}
}
/**
* @param document
* the document
* @return the document's lock object
*/
private Object getLockObject(IDocument document) {
if (document instanceof ISynchronizable) {
Object lock = ((ISynchronizable) document).getLockObject();
if (lock != null)
return lock;
}
return document;
}
/**
* Install this presenter on the given source viewer and background
* presentation reconciler.
*
* @param sourceViewer
* the source viewer
* @param backgroundPresentationReconciler
* the background presentation reconciler, can be
* <code>null</code>, in that case
* {@link SemanticHighlightingPresenter#createPresentation(List, List)}
* should not be called
*/
public void install(ScriptSourceViewer sourceViewer,
ScriptPresentationReconciler backgroundPresentationReconciler) {
fSourceViewer = sourceViewer;
fPresentationReconciler = backgroundPresentationReconciler;
fSourceViewer.prependTextPresentationListener(this);
fSourceViewer.addTextInputListener(this);
manageDocument(fSourceViewer.getDocument());
}
/**
* Uninstall this presenter.
*/
public void uninstall() {
setCanceled(true);
if (fSourceViewer != null) {
fSourceViewer.removeTextPresentationListener(this);
releaseDocument(fSourceViewer.getDocument());
invalidateTextPresentation();
resetState();
fSourceViewer.removeTextInputListener(this);
fSourceViewer = null;
}
}
/**
* Invalidate text presentation of positions with the given highlighting.
*
* @param highlighting
* The highlighting
*/
public void highlightingStyleChanged(HighlightingStyle highlighting) {
for (int i = 0, n = fPositions.size(); i < n; i++) {
HighlightedPosition position = fPositions.get(i);
if (position.getHighlighting() == highlighting)
fSourceViewer.invalidateTextPresentation(position.getOffset(),
position.getLength());
}
}
/**
* Invalidate text presentation of all positions.
*/
private void invalidateTextPresentation() {
for (int i = 0, n = fPositions.size(); i < n; i++) {
Position position = fPositions.get(i);
fSourceViewer.invalidateTextPresentation(position.getOffset(),
position.getLength());
}
}
/**
* Reset to initial state.
*/
private void resetState() {
synchronized (fPositionLock) {
fPositions.clear();
}
}
/**
* Start managing the given document.
*
* @param document
* The document
*/
private void manageDocument(IDocument document) {
if (document != null) {
document.addPositionCategory(getPositionCategory());
document.addPositionUpdater(fPositionUpdater);
document.addDocumentListener(this);
}
}
/**
* Stop managing the given document.
*
* @param document
* The document
*/
private void releaseDocument(IDocument document) {
if (document != null) {
document.removeDocumentListener(this);
document.removePositionUpdater(fPositionUpdater);
try {
document.removePositionCategory(getPositionCategory());
} catch (BadPositionCategoryException e) {
// Should not happen
DLTKUIPlugin.log(e);
}
}
}
/**
* @return The semantic reconciler position's category.
*/
private String getPositionCategory() {
return toString();
}
}