/******************************************************************************* * Copyright (c) 2000, 2008 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 *******************************************************************************/ package org.eclipse.wst.jsdt.internal.ui.javaeditor; import java.util.ArrayList; import java.util.Arrays; import java.util.List; 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; import org.eclipse.wst.jsdt.internal.ui.JavaScriptPlugin; import org.eclipse.wst.jsdt.internal.ui.javaeditor.SemanticHighlightingManager.HighlightedPosition; import org.eclipse.wst.jsdt.internal.ui.javaeditor.SemanticHighlightingManager.Highlighting; import org.eclipse.wst.jsdt.internal.ui.text.JavaPresentationReconciler; /** * Semantic highlighting presenter - UI thread implementation. * * */ public class SemanticHighlightingPresenter implements ITextPresentationListener, ITextInputListener, IDocumentListener { /** * 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 org.eclipse.jface.text.IPositionUpdater#update(org.eclipse.jface.text.DocumentEvent) */ 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 JavaSourceViewer fSourceViewer; /** The background presentation reconciler */ private JavaPresentationReconciler fPresentationReconciler; /** UI's current highlighted positions - can contain <code>null</code> elements */ private List 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 */ public HighlightedPosition createHighlightedPosition(int offset, int length, Highlighting 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 list) { synchronized (fPositionLock) { list.addAll(fPositions); } } /** * 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(List addedPositions, List removedPositions) { JavaSourceViewer sourceViewer= fSourceViewer; JavaPresentationReconciler 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.size(); i < n; i++) { Position position= (Position) removedPositions.get(i); int offset= position.getOffset(); minStart= Math.min(minStart, offset); maxEnd= Math.max(maxEnd, offset + position.getLength()); } for (int i= 0, n= addedPositions.size(); i < n; i++) { Position position= (Position) addedPositions.get(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 addedPositions the added positions * @param removedPositions the removed positions * @return the runnable or <code>null</code>, if reconciliation should be canceled */ public Runnable createUpdateRunnable(final TextPresentation textPresentation, List addedPositions, List removedPositions) { if (fSourceViewer == null || textPresentation == null) return null; // TODO: do clustering of positions and post multiple fast runnables final HighlightedPosition[] added= new SemanticHighlightingManager.HighlightedPosition[addedPositions.size()]; addedPositions.toArray(added); final SemanticHighlightingManager.HighlightedPosition[] removed= new SemanticHighlightingManager.HighlightedPosition[removedPositions.size()]; removedPositions.toArray(removed); if (isCanceled()) return null; Runnable runnable= new Runnable() { public void run() { 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 removedPositionsList= Arrays.asList(removedPositions); try { synchronized (fPositionLock) { List 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 newPositions= new ArrayList(newSize); Position position= null; Position 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= (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; } } fPositions= newPositions; } } catch (BadPositionCategoryException e) { // Should not happen JavaScriptPlugin.log(e); } catch (BadLocationException e) { // Should not happen JavaScriptPlugin.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 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 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; } /** * Insert the given position in <code>fPositions</code>, s.t. the offsets remain in linear order. * * @param position The position for insertion */ private void insertPosition(Position position) { int i= computeIndexAfterOffset(fPositions, position.getOffset()); fPositions.add(i, position); } /** * Returns the index of the first position with an offset 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 greater than the given offset */ private int computeIndexAfterOffset(List positions, int offset) { int i= -1; int j= positions.size(); while (j - i > 1) { int k= (i + j) >> 1; Position position= (Position) positions.get(k); if (position.getOffset() > offset) j= k; else i= k; } return j; } /** * 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 positions, int offset) { int i= -1; int j= positions.size(); while (j - i > 1) { int k= (i + j) >> 1; Position position= (Position) positions.get(k); if (position.getOffset() >= offset) j= k; else i= k; } return j; } /* * @see org.eclipse.jface.text.ITextPresentationListener#applyTextPresentation(org.eclipse.jface.text.TextPresentation) */ 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 ranges= new ArrayList(n - i); for (; i < n; i++) { HighlightedPosition position= (HighlightedPosition) fPositions.get(i); if (!position.isDeleted()) ranges.add(position.createStyleRange()); } StyleRange[] array= new StyleRange[ranges.size()]; array= (StyleRange[]) ranges.toArray(array); textPresentation.replaceStyleRanges(array); } else { for (; i < n; i++) { HighlightedPosition position= (HighlightedPosition) fPositions.get(i); if (!position.isDeleted()) textPresentation.replaceStyleRange(position.createStyleRange()); } } } /* * @see org.eclipse.jface.text.ITextInputListener#inputDocumentAboutToBeChanged(org.eclipse.jface.text.IDocument, org.eclipse.jface.text.IDocument) */ public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) { setCanceled(true); releaseDocument(oldInput); resetState(); } /* * @see org.eclipse.jface.text.ITextInputListener#inputDocumentChanged(org.eclipse.jface.text.IDocument, org.eclipse.jface.text.IDocument) */ public void inputDocumentChanged(IDocument oldInput, IDocument newInput) { manageDocument(newInput); } /* * @see org.eclipse.jface.text.IDocumentListener#documentAboutToBeChanged(org.eclipse.jface.text.DocumentEvent) */ public void documentAboutToBeChanged(DocumentEvent event) { setCanceled(true); } /* * @see org.eclipse.jface.text.IDocumentListener#documentChanged(org.eclipse.jface.text.DocumentEvent) */ 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(JavaSourceViewer sourceViewer, JavaPresentationReconciler 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(Highlighting highlighting) { for (int i= 0, n= fPositions.size(); i < n; i++) { HighlightedPosition position= (HighlightedPosition) 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= (Position) fPositions.get(i); fSourceViewer.invalidateTextPresentation(position.getOffset(), position.getLength()); } } /** * Add a position with the given range and highlighting unconditionally, only from UI thread. * The position will also be registered on the document. The text presentation is not invalidated. * * @param offset The range offset * @param length The range length * @param highlighting */ private void addPositionFromUI(int offset, int length, Highlighting highlighting) { Position position= createHighlightedPosition(offset, length, highlighting); synchronized (fPositionLock) { insertPosition(position); } IDocument document= fSourceViewer.getDocument(); if (document == null) return; String positionCategory= getPositionCategory(); try { document.addPosition(positionCategory, position); } catch (BadLocationException e) { // Should not happen JavaScriptPlugin.log(e); } catch (BadPositionCategoryException e) { // Should not happen JavaScriptPlugin.log(e); } } /** * 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 JavaScriptPlugin.log(e); } } } /** * @return The semantic reconciler position's category. */ private String getPositionCategory() { return toString(); } }