| /******************************************************************************* |
| * Copyright (c) 2018 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| * Red Hat Inc. - copied from SemanticHighlightingPresenter and modified |
| *******************************************************************************/ |
| |
| package org.eclipse.jdt.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.IPositionUpdater; |
| import org.eclipse.jface.text.Position; |
| |
| import org.eclipse.jdt.internal.core.manipulation.JavaManipulationPlugin; |
| |
| /** |
| * Semantic highlighting presenter - UI thread implementation. |
| * |
| * @since 1.11 |
| */ |
| public class SemanticHighlightingPresenterCore { |
| |
| /** |
| * Semantic highlighting position updater. |
| */ |
| protected 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) |
| */ |
| @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++) { |
| |
| HighlightedPositionCore position= (HighlightedPositionCore) 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(HighlightedPositionCore 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(HighlightedPositionCore 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(HighlightedPositionCore 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); |
| addPositionForEvent(event, fCategory, 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(HighlightedPositionCore 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(HighlightedPositionCore 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(HighlightedPositionCore position, DocumentEvent event) { |
| position.delete(); |
| position.update(event.getOffset(), 0); |
| } |
| } |
| |
| /** Position updater */ |
| protected IPositionUpdater fPositionUpdater= new HighlightingPositionUpdater(getPositionCategory()); |
| |
| /** UI's current highlighted positions - can contain <code>null</code> elements */ |
| protected List<Position> fPositions= new ArrayList<>(); |
| /** UI position lock */ |
| protected Object fPositionLock= new Object(); |
| |
| /** <code>true</code> iff the current reconcile is canceled. */ |
| protected 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 HighlightedPositionCore createHighlightedPositionCore(int offset, int length, Object highlighting) { |
| // TODO: reuse deleted positions |
| return new HighlightedPositionCore(offset, length, highlighting, fPositionUpdater); |
| } |
| |
| /** |
| * 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 event The document event |
| * @param category The category |
| * @param offset The range offset |
| * @param length The range length |
| * @param highlighting the highlighting |
| */ |
| protected void addPositionForEvent(DocumentEvent event, String category, int offset, int length, Object highlighting) { |
| HighlightedPositionCore highlightedPosition = new HighlightedPositionCore(offset, length, highlighting, new Object()); |
| try { |
| event.fDocument.addPosition(category, highlightedPosition); |
| } catch (BadLocationException | BadPositionCategoryException e) { |
| JavaManipulationPlugin.logException("Error when adding new highlighting position to the document", e); //$NON-NLS-1$ |
| } |
| |
| } |
| /** |
| * Adds all current positions to the given list. |
| * <p> |
| * NOTE: Called from background thread. |
| * </p> |
| * |
| * @param list The list |
| */ |
| public void addAllPositions(List<Position> list) { |
| synchronized (fPositionLock) { |
| list.addAll(fPositions); |
| } |
| } |
| |
| /** |
| * Create a runnable for updating the presentation. |
| * <p> |
| * NOTE: Called from background thread. |
| * </p> |
| * @param document the document |
| * @param addedPositions the added positions |
| * @param removedPositions the removed positions |
| * @return the runnable or <code>null</code>, if reconciliation should be canceled |
| */ |
| public Runnable createUpdateRunnableCore(IDocument document, List<Position> addedPositions, List<Position> removedPositions) { |
| |
| // TODO: do clustering of positions and post multiple fast runnables |
| final HighlightedPositionCore[] added= new HighlightedPositionCore[addedPositions.size()]; |
| addedPositions.toArray(added); |
| final HighlightedPositionCore[] removed= new HighlightedPositionCore[removedPositions.size()]; |
| removedPositions.toArray(removed); |
| |
| if (isCanceled()) |
| return null; |
| |
| Runnable runnable= new Runnable() { |
| @Override |
| public void run() { |
| updatePresentationCore(document, 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 document the document 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 updatePresentationCore(IDocument document, HighlightedPositionCore[] addedPositions, HighlightedPositionCore[] removedPositions) { |
| |
| // TODO: double-check consistency with document.getPositions(...) |
| // TODO: reuse removed positions |
| if (isCanceled()) |
| return; |
| |
| if (document == null) |
| return; |
| |
| String positionCategory= getPositionCategory(); |
| |
| List<HighlightedPositionCore> removedPositionsList= Arrays.asList(removedPositions); |
| |
| try { |
| synchronized (fPositionLock) { |
| List<Position> 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<Position> 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= 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 |
| JavaManipulationPlugin.log(e); |
| } catch (BadLocationException e) { |
| // Should not happen |
| JavaManipulationPlugin.log(e); |
| } |
| } |
| |
| /** |
| * 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 |
| */ |
| protected 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 |
| */ |
| protected 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 |
| */ |
| protected 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; |
| } |
| |
| /** |
| * @return Returns <code>true</code> iff the current reconcile is canceled. |
| * <p> |
| * NOTE: Also called from background thread. |
| * </p> |
| */ |
| public boolean isCanceled() { |
| 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) { |
| fIsCanceled = isCanceled; |
| } |
| |
| |
| /** |
| * @return The semantic reconciler position's category. |
| */ |
| protected String getPositionCategory() { |
| return toString(); |
| } |
| } |