/******************************************************************************* * Copyright (c) 2000, 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 * Tom Eicher (Avaloq Evolution AG) - block selection mode * Anton Leherbauer - [projection] Eclipse is too eager to unfold code - http://bugs.eclipse.org/178203 * Pawel Pogorzelski - [projection] ProjectionViewer.computeCollapsedNestedAnnotations unnecessary slow - http://bugs.eclipse.org/410359 *******************************************************************************/ package org.eclipse.jface.text.source.projection; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.eclipse.swt.SWTError; import org.eclipse.swt.custom.ST; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.dnd.DND; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.core.runtime.Assert; import org.eclipse.jface.internal.text.SelectionProcessor; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.FindReplaceDocumentAdapter; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentInformationMappingExtension; import org.eclipse.jface.text.IDocumentListener; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ISlaveDocumentManager; import org.eclipse.jface.text.ITextSelection; import org.eclipse.jface.text.ITextViewerExtension5; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.TextSelection; import org.eclipse.jface.text.TextUtilities; import org.eclipse.jface.text.projection.ProjectionDocument; import org.eclipse.jface.text.projection.ProjectionDocumentEvent; import org.eclipse.jface.text.projection.ProjectionDocumentManager; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.AnnotationModelEvent; import org.eclipse.jface.text.source.IAnnotationModel; import org.eclipse.jface.text.source.IAnnotationModelExtension; import org.eclipse.jface.text.source.IAnnotationModelListener; import org.eclipse.jface.text.source.IAnnotationModelListenerExtension; import org.eclipse.jface.text.source.IOverviewRuler; import org.eclipse.jface.text.source.IVerticalRuler; import org.eclipse.jface.text.source.SourceViewer; /** * A projection source viewer is a source viewer which supports multiple visible * regions which can dynamically be changed. *

* A projection source viewer uses a ProjectionDocumentManager * for the management of the visible document.

*

* NOTE: The ProjectionViewer only supports projections that cover full lines. *

*

* This class should not be subclassed.

* * @since 3.0 * @noextend This class is not intended to be subclassed by clients. */ public class ProjectionViewer extends SourceViewer implements ITextViewerExtension5 { private static final int BASE= INFORMATION; // see ISourceViewer.INFORMATION /** Operation constant for the expand operation. */ public static final int EXPAND= BASE + 1; /** Operation constant for the collapse operation. */ public static final int COLLAPSE= BASE + 2; /** Operation constant for the toggle projection operation. */ public static final int TOGGLE= BASE + 3; /** Operation constant for the expand all operation. */ public static final int EXPAND_ALL= BASE + 4; /** * Operation constant for the collapse all operation. * * @since 3.2 */ public static final int COLLAPSE_ALL= BASE + 5; /** * Internal listener to changes of the annotation model. */ private class AnnotationModelListener implements IAnnotationModelListener, IAnnotationModelListenerExtension { @Override public void modelChanged(IAnnotationModel model) { processModelChanged(model, null); } @Override public void modelChanged(AnnotationModelEvent event) { processModelChanged(event.getAnnotationModel(), event); } private void processModelChanged(IAnnotationModel model, AnnotationModelEvent event) { if (model == fProjectionAnnotationModel) { if (fProjectionSummary != null) fProjectionSummary.updateSummaries(); processCatchupRequest(event); } else if (model == getAnnotationModel() && fProjectionSummary != null) fProjectionSummary.updateSummaries(); } } /** * Executes the 'replaceVisibleDocument' operation when called the first time. Self-destructs afterwards. */ private class ReplaceVisibleDocumentExecutor implements IDocumentListener { private IDocument fSlaveDocument; private IDocument fExecutionTrigger; /** * Creates a new executor in order to free the given slave document. * * @param slaveDocument the slave document to free */ public ReplaceVisibleDocumentExecutor(IDocument slaveDocument) { fSlaveDocument= slaveDocument; } /** * Installs this executor on the given trigger document. * * @param executionTrigger the trigger document */ public void install(IDocument executionTrigger) { if (executionTrigger != null && fSlaveDocument != null) { fExecutionTrigger= executionTrigger; fExecutionTrigger.addDocumentListener(this); } } @Override public void documentAboutToBeChanged(DocumentEvent event) { } @Override public void documentChanged(DocumentEvent event) { fExecutionTrigger.removeDocumentListener(this); executeReplaceVisibleDocument(fSlaveDocument); } } /** * A command representing a change of the projection document. This can be either * adding a master document range, removing a master document change, or invalidating * the viewer text presentation. */ private static class ProjectionCommand { final static int ADD= 0; final static int REMOVE= 1; final static int INVALIDATE_PRESENTATION= 2; ProjectionDocument fProjection; int fType; int fOffset; int fLength; ProjectionCommand(ProjectionDocument projection, int type, int offset, int length) { fProjection= projection; fType= type; fOffset= offset; fLength= length; } ProjectionCommand(int offset, int length) { fType= INVALIDATE_PRESENTATION; fOffset= offset; fLength= length; } int computeExpectedCosts() { switch(fType) { case ADD: { try { IRegion[] gaps= fProjection.computeUnprojectedMasterRegions(fOffset, fLength); return gaps == null ? 0 : gaps.length; } catch (BadLocationException x) { } break; } case REMOVE: { try { IRegion[] fragments= fProjection.computeProjectedMasterRegions(fOffset, fLength); return fragments == null ? 0 : fragments.length; } catch (BadLocationException x) { } break; } } return 0; } } /** * The queue of projection command objects. */ private static class ProjectionCommandQueue { final static int REDRAW_COSTS= 15; final static int INVALIDATION_COSTS= 10; List fList= new ArrayList<>(15); int fExpectedExecutionCosts= -1; void add(ProjectionCommand command) { fList.add(command); } Iterator iterator() { return fList.iterator(); } void clear() { fList.clear(); fExpectedExecutionCosts= -1; } boolean passedRedrawCostsThreshold() { if (fExpectedExecutionCosts == -1) computeExpectedExecutionCosts(); return fExpectedExecutionCosts > REDRAW_COSTS; } boolean passedInvalidationCostsThreshold() { if (fExpectedExecutionCosts == -1) computeExpectedExecutionCosts(); return fExpectedExecutionCosts > INVALIDATION_COSTS; } private void computeExpectedExecutionCosts() { int max_costs= Math.max(REDRAW_COSTS, INVALIDATION_COSTS); fExpectedExecutionCosts= fList.size(); if (fExpectedExecutionCosts <= max_costs) { ProjectionCommand command; Iterator e= fList.iterator(); while (e.hasNext()) { command= e.next(); fExpectedExecutionCosts += command.computeExpectedCosts(); if (fExpectedExecutionCosts > max_costs) break; } } } } /** The projection annotation model used by this viewer. */ private ProjectionAnnotationModel fProjectionAnnotationModel; /** The annotation model listener */ private IAnnotationModelListener fAnnotationModelListener= new AnnotationModelListener(); /** The projection summary. */ private ProjectionSummary fProjectionSummary; /** Indication that an annotation world change has not yet been processed. */ private boolean fPendingAnnotationWorldChange= false; /** Indication whether projection changes in the visible document should be considered. */ private boolean fHandleProjectionChanges= true; /** The list of projection listeners. */ private List fProjectionListeners; /** Internal lock for protecting the list of pending requests */ private Object fLock= new Object(); /** The list of pending requests */ private List fPendingRequests= new ArrayList<>(); /** The replace-visible-document execution trigger */ private IDocument fReplaceVisibleDocumentExecutionTrigger; /** true if projection was on the last time we switched to segmented mode. */ private boolean fWasProjectionEnabled; /** The queue of projection commands used to assess the costs of projection changes. */ private ProjectionCommandQueue fCommandQueue; /** * The amount of lines deleted by the last document event issued by the * visible document event. * @since 3.1 */ private int fDeletedLines; /** * Creates a new projection source viewer. * * @param parent the SWT parent control * @param ruler the vertical ruler * @param overviewRuler the overview ruler * @param showsAnnotationOverview true if the overview ruler should be shown * @param styles the SWT style bits */ public ProjectionViewer(Composite parent, IVerticalRuler ruler, IOverviewRuler overviewRuler, boolean showsAnnotationOverview, int styles) { super(parent, ruler, overviewRuler, showsAnnotationOverview, styles); } /** * Sets the projection summary for this viewer. * * @param projectionSummary the projection summary. */ public void setProjectionSummary(ProjectionSummary projectionSummary) { fProjectionSummary= projectionSummary; } /** * Adds the projection annotation model to the given annotation model. * * @param model the model to which the projection annotation model is added */ private void addProjectionAnnotationModel(IAnnotationModel model) { if (model instanceof IAnnotationModelExtension) { IAnnotationModelExtension extension= (IAnnotationModelExtension) model; extension.addAnnotationModel(ProjectionSupport.PROJECTION, fProjectionAnnotationModel); model.addAnnotationModelListener(fAnnotationModelListener); } } /** * Removes the projection annotation model from the given annotation model. * * @param model the mode from which the projection annotation model is removed * @return the removed projection annotation model or null if there was none */ private IAnnotationModel removeProjectionAnnotationModel(IAnnotationModel model) { if (model instanceof IAnnotationModelExtension) { model.removeAnnotationModelListener(fAnnotationModelListener); IAnnotationModelExtension extension= (IAnnotationModelExtension) model; return extension.removeAnnotationModel(ProjectionSupport.PROJECTION); } return null; } @Override public void setDocument(IDocument document, IAnnotationModel annotationModel, int modelRangeOffset, int modelRangeLength) { boolean wasProjectionEnabled= false; synchronized (fLock) { fPendingRequests.clear(); } if (fProjectionAnnotationModel != null) { wasProjectionEnabled= removeProjectionAnnotationModel(getVisualAnnotationModel()) != null; fProjectionAnnotationModel= null; } super.setDocument(document, annotationModel, modelRangeOffset, modelRangeLength); if (wasProjectionEnabled && document != null) enableProjection(); } @Override protected IAnnotationModel createVisualAnnotationModel(IAnnotationModel annotationModel) { IAnnotationModel model= super.createVisualAnnotationModel(annotationModel); fProjectionAnnotationModel= new ProjectionAnnotationModel(); return model; } /** * Returns the projection annotation model. * * @return the projection annotation model */ public ProjectionAnnotationModel getProjectionAnnotationModel() { IAnnotationModel model= getVisualAnnotationModel(); if (model instanceof IAnnotationModelExtension) { IAnnotationModelExtension extension= (IAnnotationModelExtension) model; return (ProjectionAnnotationModel) extension.getAnnotationModel(ProjectionSupport.PROJECTION); } return null; } @Override protected ISlaveDocumentManager createSlaveDocumentManager() { return new ProjectionDocumentManager(); } @Override protected boolean updateSlaveDocument(IDocument slaveDocument, int modelRangeOffset, int modelRangeLength) throws BadLocationException { if (slaveDocument instanceof ProjectionDocument) { ProjectionDocument projection= (ProjectionDocument) slaveDocument; int offset= modelRangeOffset; int length= modelRangeLength; if (!isProjectionMode()) { // mimic original TextViewer behavior IDocument master= projection.getMasterDocument(); int line= master.getLineOfOffset(modelRangeOffset); offset= master.getLineOffset(line); length= (modelRangeOffset - offset) + modelRangeLength; } try { fHandleProjectionChanges= false; projection.replaceMasterDocumentRanges(offset, length); } finally { fHandleProjectionChanges= true; } return true; } return false; } /** * Adds a projection annotation listener to this viewer. The listener may * not be null. If the listener is already registered, this method * does not have any effect. * * @param listener the listener to add */ public void addProjectionListener(IProjectionListener listener) { Assert.isNotNull(listener); if (fProjectionListeners == null) fProjectionListeners= new ArrayList<>(); if (!fProjectionListeners.contains(listener)) fProjectionListeners.add(listener); } /** * Removes the given listener from this viewer. The listener may not be * null. If the listener is not registered with this viewer, * this method is without effect. * * @param listener the listener to remove */ public void removeProjectionListener(IProjectionListener listener) { Assert.isNotNull(listener); if (fProjectionListeners != null) { fProjectionListeners.remove(listener); if (fProjectionListeners.size() == 0) fProjectionListeners= null; } } /** * Notifies all registered projection listeners * that projection mode has been enabled. */ protected void fireProjectionEnabled() { if (fProjectionListeners != null) { Iterator e= new ArrayList<>(fProjectionListeners).iterator(); while (e.hasNext()) { IProjectionListener l= e.next(); l.projectionEnabled(); } } } /** * Notifies all registered projection listeners * that projection mode has been disabled. */ protected void fireProjectionDisabled() { if (fProjectionListeners != null) { Iterator e= new ArrayList<>(fProjectionListeners).iterator(); while (e.hasNext()) { IProjectionListener l= e.next(); l.projectionDisabled(); } } } /** * Returns whether this viewer is in projection mode. * * @return true if this viewer is in projection mode, * false otherwise */ public final boolean isProjectionMode() { return getProjectionAnnotationModel() != null; } /** * Disables the projection mode. */ public final void disableProjection() { if (isProjectionMode()) { removeProjectionAnnotationModel(getVisualAnnotationModel()); fProjectionAnnotationModel.removeAllAnnotations(); fFindReplaceDocumentAdapter= null; fireProjectionDisabled(); } } /** * Enables the projection mode. */ public final void enableProjection() { if (!isProjectionMode()) { addProjectionAnnotationModel(getVisualAnnotationModel()); fFindReplaceDocumentAdapter= null; fireProjectionEnabled(); } } private void expandAll() { int offset= 0; IDocument doc= getDocument(); int length= doc == null ? 0 : doc.getLength(); if (isProjectionMode()) { fProjectionAnnotationModel.expandAll(offset, length); } } private void expand() { if (isProjectionMode()) { Position found= null; Annotation bestMatch= null; Point selection= getSelectedRange(); for (Iterator e= fProjectionAnnotationModel.getAnnotationIterator(); e.hasNext();) { ProjectionAnnotation annotation= (ProjectionAnnotation) e.next(); if (annotation.isCollapsed()) { Position position= fProjectionAnnotationModel.getPosition(annotation); // take the first most fine grained match if (position != null && touches(selection, position)) if (found == null || position.includes(found.offset) && position.includes(found.offset + found.length)) { found= position; bestMatch= annotation; } } } if (bestMatch != null) { fProjectionAnnotationModel.expand(bestMatch); revealRange(selection.x, selection.y); } } } private boolean touches(Point selection, Position position) { return position.overlapsWith(selection.x, selection.y) || selection.y == 0 && position.offset + position.length == selection.x + selection.y; } private void collapse() { if (isProjectionMode()) { Position found= null; Annotation bestMatch= null; Point selection= getSelectedRange(); for (Iterator e= fProjectionAnnotationModel.getAnnotationIterator(); e.hasNext();) { ProjectionAnnotation annotation= (ProjectionAnnotation) e.next(); if (!annotation.isCollapsed()) { Position position= fProjectionAnnotationModel.getPosition(annotation); // take the first most fine grained match if (position != null && touches(selection, position)) if (found == null || found.includes(position.offset) && found.includes(position.offset + position.length)) { found= position; bestMatch= annotation; } } } if (bestMatch != null) { fProjectionAnnotationModel.collapse(bestMatch); revealRange(selection.x, selection.y); } } } /* * @since 3.2 */ private void collapseAll() { int offset= 0; IDocument doc= getDocument(); int length= doc == null ? 0 : doc.getLength(); if (isProjectionMode()) { fProjectionAnnotationModel.collapseAll(offset, length); } } /** * Adds the given master range to the given projection document. While the * modification is processed, the viewer no longer handles projection * changes, as it is causing them. * * @param projection the projection document * @param offset the offset in the master document * @param length the length in the master document * @throws BadLocationException in case the specified range is invalid * * @see ProjectionDocument#addMasterDocumentRange(int, int) */ private void addMasterDocumentRange(ProjectionDocument projection, int offset, int length) throws BadLocationException { if (fCommandQueue != null) { fCommandQueue.add(new ProjectionCommand(projection, ProjectionCommand.ADD, offset, length)); } else { try { fHandleProjectionChanges= false; // https://bugs.eclipse.org/bugs/show_bug.cgi?id=108258 // make sure the document range is strictly line based int end= offset + length; offset= toLineStart(projection.getMasterDocument(), offset, false); length= toLineStart(projection.getMasterDocument(), end, true) - offset; projection.addMasterDocumentRange(offset, length); } finally { fHandleProjectionChanges= true; } } } /** * Removes the given master range from the given projection document. While the * modification is processed, the viewer no longer handles projection * changes, as it is causing them. * * @param projection the projection document * @param offset the offset in the master document * @param length the length in the master document * @throws BadLocationException in case the specified range is invalid * * @see ProjectionDocument#removeMasterDocumentRange(int, int) */ private void removeMasterDocumentRange(ProjectionDocument projection, int offset, int length) throws BadLocationException { if (fCommandQueue != null) { fCommandQueue.add(new ProjectionCommand(projection, ProjectionCommand.REMOVE, offset, length)); } else { try { fHandleProjectionChanges= false; // https://bugs.eclipse.org/bugs/show_bug.cgi?id=108258 // make sure the document range is strictly line based int end= offset + length; offset= toLineStart(projection.getMasterDocument(), offset, false); length= toLineStart(projection.getMasterDocument(), end, true) - offset; projection.removeMasterDocumentRange(offset, length); } finally { fHandleProjectionChanges= true; } } } /** * Returns the first line offset <= offset. If testLastLine * is true and the offset is on last line then offset is returned. * * @param document the document * @param offset the master document offset * @param testLastLine true if the test for the last line should be performed * @return the closest line offset >= offset * @throws BadLocationException if the offset is invalid * @since 3.2 */ private int toLineStart(IDocument document, int offset, boolean testLastLine) throws BadLocationException { if (document == null) return offset; if (testLastLine && offset >= document.getLineInformationOfOffset(document.getLength() - 1).getOffset()) return offset; return document.getLineInformationOfOffset(offset).getOffset(); } @Override public void setVisibleRegion(int start, int length) { fWasProjectionEnabled= isProjectionMode(); disableProjection(); super.setVisibleRegion(start, length); } @Override protected void setVisibleDocument(IDocument document) { if (!isProjectionMode()) { super.setVisibleDocument(document); return; } // In projection mode we don't want to throw away the find/replace document adapter FindReplaceDocumentAdapter adapter= fFindReplaceDocumentAdapter; super.setVisibleDocument(document); fFindReplaceDocumentAdapter= adapter; } @Override public void resetVisibleRegion() { super.resetVisibleRegion(); if (fWasProjectionEnabled) enableProjection(); } @Override public IRegion getVisibleRegion() { disableProjection(); IRegion visibleRegion= getModelCoverage(); if (visibleRegion == null) visibleRegion= new Region(0, 0); return visibleRegion; } @Override public boolean overlapsWithVisibleRegion(int offset, int length) { disableProjection(); IRegion coverage= getModelCoverage(); if (coverage == null) return false; boolean appending= (offset == coverage.getOffset() + coverage.getLength()) && length == 0; return appending || TextUtilities.overlaps(coverage, new Region(offset, length)); } /** * Replace the visible document with the given document. Maintains the * scroll offset and the selection. * * @param slave the visible document */ private void replaceVisibleDocument(IDocument slave) { if (fReplaceVisibleDocumentExecutionTrigger != null) { ReplaceVisibleDocumentExecutor executor= new ReplaceVisibleDocumentExecutor(slave); executor.install(fReplaceVisibleDocumentExecutionTrigger); } else executeReplaceVisibleDocument(slave); } private void executeReplaceVisibleDocument(IDocument visibleDocument) { StyledText textWidget= getTextWidget(); try { if (textWidget != null && !textWidget.isDisposed()) textWidget.setRedraw(false); int topIndex= getTopIndex(); Point selection= getSelectedRange(); setVisibleDocument(visibleDocument); Point currentSelection= getSelectedRange(); if (currentSelection.x != selection.x || currentSelection.y != selection.y) setSelectedRange(selection.x, selection.y); setTopIndex(topIndex); } finally { if (textWidget != null && !textWidget.isDisposed()) textWidget.setRedraw(true); } } /** * Hides the given range by collapsing it. If requested, a redraw request is issued. * * @param offset the offset of the range to hide * @param length the length of the range to hide * @param fireRedraw true if a redraw request should be issued, false otherwise * @throws BadLocationException in case the range is invalid */ private void collapse(int offset, int length, boolean fireRedraw) throws BadLocationException { ProjectionDocument projection= null; IDocument visibleDocument= getVisibleDocument(); if (visibleDocument instanceof ProjectionDocument) projection= (ProjectionDocument) visibleDocument; else { IDocument master= getDocument(); IDocument slave= createSlaveDocument(getDocument()); if (slave instanceof ProjectionDocument) { projection= (ProjectionDocument) slave; addMasterDocumentRange(projection, 0, master.getLength()); replaceVisibleDocument(projection); } } if (projection != null) removeMasterDocumentRange(projection, offset, length); if (projection != null && fireRedraw) { // repaint line above to get the folding box IDocument document= getDocument(); int line= document.getLineOfOffset(offset); if (line > 0) { IRegion info= document.getLineInformation(line - 1); internalInvalidateTextPresentation(info.getOffset(), info.getLength()); } } } /** * Makes the given range visible again while not changing the folding state of any contained * ranges. If requested, a redraw request is issued. * * @param offset the offset of the range to be expanded * @param length the length of the range to be expanded * @param fireRedraw true if a redraw request should be issued, * false otherwise * @throws BadLocationException in case the range is invalid */ private void expand(int offset, int length, boolean fireRedraw) throws BadLocationException { IDocument slave= getVisibleDocument(); if (slave instanceof ProjectionDocument) { ProjectionDocument projection= (ProjectionDocument) slave; // expand addMasterDocumentRange(projection, offset, length); // collapse contained regions ProjectionAnnotation[] collapsed= computeCollapsedNestedAnnotations(offset, length); if (collapsed != null) { for (int i= 0; i < collapsed.length; i++) { IRegion[] regions= computeCollapsedRegions(fProjectionAnnotationModel.getPosition(collapsed[i])); if (regions != null) for (int j= 0; j < regions.length; j++) removeMasterDocumentRange(projection, regions[j].getOffset(), regions[j].getLength()); } } // redraw if requested if (fireRedraw) internalInvalidateTextPresentation(offset, length); } } /** * Processes the request for catch up with the annotation model in the UI thread. If the current * thread is not the UI thread or there are pending catch up requests, a new request is posted. * * @param event the annotation model event */ protected final void processCatchupRequest(AnnotationModelEvent event) { if (Display.getCurrent() != null) { boolean run= false; synchronized (fLock) { run= fPendingRequests.isEmpty(); } if (run) { try { catchupWithProjectionAnnotationModel(event); } catch (BadLocationException x) { throw new IllegalArgumentException(); } } else postCatchupRequest(event); } else { postCatchupRequest(event); } } /** * Posts the request for catch up with the annotation model into the UI thread. * * @param event the annotation model event */ protected final void postCatchupRequest(final AnnotationModelEvent event) { synchronized (fLock) { fPendingRequests.add(event); if (fPendingRequests.size() == 1) { StyledText widget= getTextWidget(); if (widget != null) { Display display= widget.getDisplay(); if (display != null) { display.asyncExec(() -> { try { while (true) { AnnotationModelEvent ame= null; synchronized (fLock) { if (fPendingRequests.size() == 0) return; ame= fPendingRequests.remove(0); } catchupWithProjectionAnnotationModel(ame); } } catch (BadLocationException x) { try { catchupWithProjectionAnnotationModel(null); } catch (BadLocationException x1) { throw new IllegalArgumentException(); } finally { synchronized (fLock) { fPendingRequests.clear(); } } } }); } } } } } /** * Tests whether the visible document's master document * is identical to this viewer's document. * * @return true if the visible document's master is * identical to this viewer's document * @since 3.1 */ private boolean isVisibleMasterDocumentSameAsDocument() { IDocument visibleDocument= getVisibleDocument(); return (visibleDocument instanceof ProjectionDocument) && ((ProjectionDocument)visibleDocument).getMasterDocument() == getDocument(); } /** * Adapts the slave visual document of this viewer to the changes described * in the annotation model event. When the event is null, * this is identical to a world change event. * * @param event the annotation model event or null * @exception BadLocationException in case the annotation model event is no longer in synchronization with the document */ private void catchupWithProjectionAnnotationModel(AnnotationModelEvent event) throws BadLocationException { if (event == null || !isVisibleMasterDocumentSameAsDocument()) { fPendingAnnotationWorldChange= false; reinitializeProjection(); } else if (event.isWorldChange()) { if (event.isValid()) { fPendingAnnotationWorldChange= false; reinitializeProjection(); } else fPendingAnnotationWorldChange= true; } else if (fPendingAnnotationWorldChange) { if (event.isValid()) { fPendingAnnotationWorldChange= false; reinitializeProjection(); } } else { Annotation[] addedAnnotations= event.getAddedAnnotations(); Annotation[] changedAnnotation= event.getChangedAnnotations(); Annotation[] removedAnnotations= event.getRemovedAnnotations(); fCommandQueue= new ProjectionCommandQueue(); boolean isRedrawing= redraws(); int topIndex= isRedrawing ? getTopIndex() : -1; processDeletions(event, removedAnnotations, true); List coverage= new ArrayList<>(); processChanges(addedAnnotations, true, coverage); processChanges(changedAnnotation, true, coverage); ProjectionCommandQueue commandQueue= fCommandQueue; fCommandQueue= null; if (commandQueue.passedRedrawCostsThreshold()) { setRedraw(false); try { executeProjectionCommands(commandQueue, false); } catch (IllegalArgumentException x) { reinitializeProjection(); } finally { setRedraw(true, topIndex); } } else { try { boolean fireRedraw= !commandQueue.passedInvalidationCostsThreshold(); executeProjectionCommands(commandQueue, fireRedraw); if (!fireRedraw) invalidateTextPresentation(); } catch (IllegalArgumentException x) { reinitializeProjection(); } } } } private void executeProjectionCommands(ProjectionCommandQueue commandQueue, boolean fireRedraw) throws BadLocationException { ProjectionCommand command; Iterator e= commandQueue.iterator(); while (e.hasNext()) { command= e.next(); switch (command.fType) { case ProjectionCommand.ADD: addMasterDocumentRange(command.fProjection, command.fOffset, command.fLength); break; case ProjectionCommand.REMOVE: removeMasterDocumentRange(command.fProjection, command.fOffset, command.fLength); break; case ProjectionCommand.INVALIDATE_PRESENTATION: if (fireRedraw) invalidateTextPresentation(command.fOffset, command.fLength); break; } } commandQueue.clear(); } private ProjectionAnnotation[] computeCollapsedNestedAnnotations(int offset, int length) { List annotations= new ArrayList<>(5); Iterator e= fProjectionAnnotationModel.getAnnotationIterator(offset, length, false, false); while (e.hasNext()) { ProjectionAnnotation annotation= (ProjectionAnnotation) e.next(); if (annotation.isCollapsed()) { Position position= fProjectionAnnotationModel.getPosition(annotation); if (position == null) { // annotation might already be deleted, we will be informed later on about this deletion continue; } annotations.add(annotation); } } if (annotations.size() > 0) { ProjectionAnnotation[] result= new ProjectionAnnotation[annotations.size()]; annotations.toArray(result); return result; } return null; } private void internalInvalidateTextPresentation(int offset, int length) { if (fCommandQueue != null) { fCommandQueue.add(new ProjectionCommand(offset, length)); } else { invalidateTextPresentation(offset, length); } } /* * We pass the removed annotation into this method for performance reasons only. Otherwise, they could be fetch from the event. */ private void processDeletions(AnnotationModelEvent event, Annotation[] removedAnnotations, boolean fireRedraw) throws BadLocationException { for (int i= 0; i < removedAnnotations.length; i++) { ProjectionAnnotation annotation= (ProjectionAnnotation) removedAnnotations[i]; if (annotation.isCollapsed()) { Position expanded= event.getPositionOfRemovedAnnotation(annotation); expand(expanded.getOffset(), expanded.getLength(), fireRedraw); } } } /** * Computes the region that must be collapsed when the given position is the * position of an expanded projection annotation. * * @param position the position * @return the range that must be collapsed */ public IRegion computeCollapsedRegion(Position position) { try { IDocument document= getDocument(); if (document == null) return null; int line= document.getLineOfOffset(position.getOffset()); int offset= document.getLineOffset(line + 1); int length= position.getLength() - (offset - position.getOffset()); if (length > 0) return new Region(offset, length); } catch (BadLocationException x) { } return null; } /** * Computes the regions that must be collapsed when the given position is * the position of an expanded projection annotation. * * @param position the position * @return the ranges that must be collapsed, or null if * there are none * @since 3.1 */ IRegion[] computeCollapsedRegions(Position position) { try { IDocument document= getDocument(); if (document == null) return null; if (position instanceof IProjectionPosition) { IProjectionPosition projPosition= (IProjectionPosition) position; return projPosition.computeProjectionRegions(document); } int line= document.getLineOfOffset(position.getOffset()); int offset= document.getLineOffset(line + 1); int length= position.getLength() - (offset - position.getOffset()); if (length > 0) return new IRegion[] {new Region(offset, length)}; return null; } catch (BadLocationException x) { return null; } } /** * Computes the collapsed region anchor for the given position. Assuming * that the position is the position of an expanded projection annotation, * the anchor is the region that is still visible after the projection * annotation has been collapsed. * * @param position the position * @return the collapsed region anchor */ public Position computeCollapsedRegionAnchor(Position position) { try { IDocument document= getDocument(); if (document == null) return null; int captionOffset= position.getOffset(); if (position instanceof IProjectionPosition) captionOffset+= ((IProjectionPosition) position).computeCaptionOffset(document); IRegion lineInfo= document.getLineInformationOfOffset(captionOffset); return new Position(lineInfo.getOffset() + lineInfo.getLength(), 0); } catch (BadLocationException x) { } return null; } private void processChanges(Annotation[] annotations, boolean fireRedraw, List coverage) throws BadLocationException { for (int i= 0; i < annotations.length; i++) { ProjectionAnnotation annotation= (ProjectionAnnotation) annotations[i]; Position position= fProjectionAnnotationModel.getPosition(annotation); if (position == null) continue; if (!covers(coverage, position)) { if (annotation.isCollapsed()) { coverage.add(position); IRegion[] regions= computeCollapsedRegions(position); if (regions != null) for (int j= 0; j < regions.length; j++) collapse(regions[j].getOffset(), regions[j].getLength(), fireRedraw); } else { expand(position.getOffset(), position.getLength(), fireRedraw); } } } } private boolean covers(List coverage, Position position) { Iterator e= coverage.iterator(); while (e.hasNext()) { Position p= e.next(); if (p.getOffset() <= position.getOffset() && position.getOffset() + position.getLength() <= p.getOffset() + p.getLength()) return true; } return false; } /** * Forces this viewer to throw away any old state and to initialize its content * from its projection annotation model. * * @throws BadLocationException in case something goes wrong during initialization */ public final void reinitializeProjection() throws BadLocationException { ProjectionDocument projection= null; ISlaveDocumentManager manager= getSlaveDocumentManager(); if (manager != null) { IDocument master= getDocument(); if (master != null) { IDocument slave= manager.createSlaveDocument(master); if (slave instanceof ProjectionDocument) { projection= (ProjectionDocument) slave; addMasterDocumentRange(projection, 0, master.getLength()); } } } if (projection != null) { Iterator e= fProjectionAnnotationModel.getAnnotationIterator(); while (e.hasNext()) { ProjectionAnnotation annotation= (ProjectionAnnotation) e.next(); if (annotation.isCollapsed()) { Position position= fProjectionAnnotationModel.getPosition(annotation); if (position != null) { IRegion[] regions= computeCollapsedRegions(position); if (regions != null) for (int i= 0; i < regions.length; i++) removeMasterDocumentRange(projection, regions[i].getOffset(), regions[i].getLength()); } } } } replaceVisibleDocument(projection); } @Override protected void handleVerifyEvent(VerifyEvent e) { if (getTextWidget().getBlockSelection()) { ITextSelection selection= (ITextSelection) getSelection(); if (exposeModelRange(new Region(selection.getOffset(), selection.getLength()))) setSelection(selection); super.handleVerifyEvent(e); return; } Point selection= getSelectedRange(); IRegion modelRange= event2ModelRange(e); if (exposeModelRange(modelRange)) { e.doit= false; try { if (selection.y == 0 && e.text.length() <= 1 && modelRange.getLength() == 1) { selection.y= 1; if (selection.x - 1 == modelRange.getOffset()) selection.x--; } getDocument().replace(selection.x, selection.y, e.text); setSelectedRange(selection.x + e.text.length(), 0); } catch (BadLocationException e1) { // ignore as nothing bad happens (no log at this level) } } else super.handleVerifyEvent(e); } @Override public boolean exposeModelRange(IRegion modelRange) { if (isProjectionMode()) return fProjectionAnnotationModel.expandAll(modelRange.getOffset(), modelRange.getLength()); if (!overlapsWithVisibleRegion(modelRange.getOffset(), modelRange.getLength())) { resetVisibleRegion(); return true; } return false; } @Override public void setRangeIndication(int offset, int length, boolean moveCursor) { IRegion rangeIndication= getRangeIndication(); if (moveCursor && fProjectionAnnotationModel != null && (rangeIndication == null || offset != rangeIndication.getOffset() || length != rangeIndication.getLength())) { List expand= new ArrayList<>(2); // expand the immediate affected collapsed regions Iterator iterator= fProjectionAnnotationModel.getAnnotationIterator(); while (iterator.hasNext()) { ProjectionAnnotation annotation= (ProjectionAnnotation)iterator.next(); if (annotation.isCollapsed() && willAutoExpand(fProjectionAnnotationModel.getPosition(annotation), offset, length)) expand.add(annotation); } if (!expand.isEmpty()) { Iterator e= expand.iterator(); while (e.hasNext()) fProjectionAnnotationModel.expand(e.next()); } } super.setRangeIndication(offset, length, moveCursor); } private boolean willAutoExpand(Position position, int offset, int length) { if (position == null || position.isDeleted()) return false; // right or left boundary if (position.getOffset() == offset || position.getOffset() + position.getLength() == offset + length) return true; // completely embedded in given position if (position.getOffset() < offset && offset + length < position.getOffset() + position.getLength()) return true; return false; } @Override protected void handleDispose() { fWasProjectionEnabled= false; super.handleDispose(); } /* * @see org.eclipse.jface.text.TextViewer#handleVisibleDocumentAboutToBeChanged(org.eclipse.jface.text.DocumentEvent) */ @Override protected void handleVisibleDocumentChanged(DocumentEvent event) { if (fHandleProjectionChanges && event instanceof ProjectionDocumentEvent && isProjectionMode()) { ProjectionDocumentEvent e= (ProjectionDocumentEvent) event; DocumentEvent master= e.getMasterEvent(); if (master != null) fReplaceVisibleDocumentExecutionTrigger= master.getDocument(); try { int replaceLength= e.getText() == null ? 0 : e.getText().length(); if (ProjectionDocumentEvent.PROJECTION_CHANGE == e.getChangeType()) { if (e.getLength() == 0 && replaceLength != 0) fProjectionAnnotationModel.expandAll(e.getMasterOffset(), e.getMasterLength()); } else if (master != null && (replaceLength > 0 || fDeletedLines > 1)) { try { int numberOfLines= e.getDocument().getNumberOfLines(e.getOffset(), replaceLength); if (numberOfLines > 1 || fDeletedLines > 1) fProjectionAnnotationModel.expandAll(master.getOffset(), replaceLength); } catch (BadLocationException x) { } } } finally { fReplaceVisibleDocumentExecutionTrigger= null; } } } @Override protected void handleVisibleDocumentAboutToBeChanged(DocumentEvent event) { if (fHandleProjectionChanges && event instanceof ProjectionDocumentEvent && isProjectionMode()) { int deletedLines; try { deletedLines= event.getDocument().getNumberOfLines(event.getOffset(), event.getLength()); } catch (BadLocationException e1) { deletedLines= 0; } fDeletedLines= deletedLines; } } @Override public IRegion[] getCoveredModelRanges(IRegion modelRange) { if (fInformationMapping == null) return new IRegion[] { new Region(modelRange.getOffset(), modelRange.getLength()) }; if (fInformationMapping instanceof IDocumentInformationMappingExtension) { IDocumentInformationMappingExtension extension= (IDocumentInformationMappingExtension) fInformationMapping; try { return extension.getExactCoverage(modelRange); } catch (BadLocationException x) { } } return null; } @Override public void doOperation(int operation) { switch (operation) { case TOGGLE: if (canDoOperation(TOGGLE)) { if (!isProjectionMode()) { enableProjection(); } else { expandAll(); disableProjection(); } return; } } if (!isProjectionMode()) { super.doOperation(operation); return; } StyledText textWidget= getTextWidget(); if (textWidget == null) return; ITextSelection selection= null; switch (operation) { case CUT: if (redraws()) { selection= (ITextSelection) getSelection(); if (exposeModelRange(new Region(selection.getOffset(), selection.getLength()))) setSelection(selection); if (selection.getLength() == 0) copyMarkedRegion(true); else copyToClipboard(selection, true, textWidget); Point range= textWidget.getSelectionRange(); fireSelectionChanged(range.x, range.y); } break; case COPY: if (redraws()) { selection= (ITextSelection) getSelection(); if (selection.getLength() == 0) copyMarkedRegion(false); else copyToClipboard(selection, false, textWidget); } break; case DELETE: if (redraws()) { try { selection= (ITextSelection) getSelection(); int length= selection.getLength(); if (!textWidget.getBlockSelection() && (length == 0 || length == textWidget.getSelectionRange().y)) getTextWidget().invokeAction(ST.DELETE_NEXT); else deleteSelection(selection, textWidget); Point range= textWidget.getSelectionRange(); fireSelectionChanged(range.x, range.y); } catch (BadLocationException x) { // ignore } } break; case EXPAND_ALL: if (redraws()) expandAll(); break; case EXPAND: if (redraws()) { expand(); } break; case COLLAPSE_ALL: if (redraws()) collapseAll(); break; case COLLAPSE: if (redraws()) { collapse(); } break; default: super.doOperation(operation); } } @Override public boolean canDoOperation(int operation) { switch (operation) { case COLLAPSE: case COLLAPSE_ALL: case EXPAND: case EXPAND_ALL: return isProjectionMode(); case TOGGLE: return isProjectionMode() || !isSegmented(); } return super.canDoOperation(operation); } private boolean isSegmented() { IDocument document= getDocument(); int length= document == null ? 0 : document.getLength(); IRegion visible= getModelCoverage(); boolean isSegmented= visible != null && !visible.equals(new Region(0, length)); return isSegmented; } private IRegion getMarkedRegion() { if (getTextWidget() == null) return null; if (fMarkPosition == null || fMarkPosition.isDeleted()) return null; int start= fMarkPosition.getOffset(); int end= getSelectedRange().x; return start > end ? new Region (end, start - end) : new Region(start, end - start); } @Override protected void copyMarkedRegion(boolean delete) { IRegion markedRegion= getMarkedRegion(); if (markedRegion != null) copyToClipboard(new TextSelection(getDocument(), markedRegion.getOffset(), markedRegion.getLength()), delete, getTextWidget()); } private void copyToClipboard(ITextSelection selection, boolean delete, StyledText textWidget) { String copyText= selection.getText(); if (copyText == null) // selection.getText failed - backup using widget textWidget.copy(); if (copyText != null && copyText.equals(textWidget.getSelectionText())) { /* * XXX: Reduce pain of https://bugs.eclipse.org/bugs/show_bug.cgi?id=64498 * by letting the widget handle the copy operation in this special case. */ textWidget.copy(); } else if (copyText != null) { Clipboard clipboard= new Clipboard(textWidget.getDisplay()); try { Transfer[] dataTypes= new Transfer[] { TextTransfer.getInstance() }; Object[] data= new Object[] { copyText }; try { clipboard.setContents(data, dataTypes); } catch (SWTError e) { if (e.code != DND.ERROR_CANNOT_SET_CLIPBOARD) throw e; /* * TODO see https://bugs.eclipse.org/bugs/show_bug.cgi?id=59459 * we should either log and/or inform the user * silently fail for now. */ return; } } finally { clipboard.dispose(); } } if (delete) { try { deleteSelection(selection, textWidget); } catch (BadLocationException x) { // XXX: should log here, but JFace Text has no Log } } } /** * Deletes the selection and sets the caret before the deleted range. * * @param selection the selection to delete * @param textWidget the widget * @throws BadLocationException on document access failure * @since 3.5 */ private void deleteSelection(ITextSelection selection, StyledText textWidget) throws BadLocationException { new SelectionProcessor(this).doDelete(selection); } /** * Adapts the behavior of the super class to respect line based folding. * * @param widgetSelection the widget selection * @return the model selection while respecting line based folding */ @Override protected Point widgetSelection2ModelSelection(Point widgetSelection) { if (!isProjectionMode()) return super.widgetSelection2ModelSelection(widgetSelection); /* * There is one requirement that governs preservation of logical * positions: * * 1) a selection with widget_length == 0 should never expand to have * model_length > 0. * * There are a number of ambiguities to resolve with projection regions. * A projected region P has a widget-length of zero. Its widget offset * may interact with the selection S in various ways: * * A) P.widget_offset lies at the caret, S.widget_length is zero. * Requirement 1 applies. S is *behind* P (done so by widgetRange2ModelRange). * * B) P.widget_offset lies inside the widget selection. This case is * easy: P is included in S, which is automatically done so by * widgetRange2ModelRange. * * C) P.widget_offset lies at S.widget_end: This is * arguable - our policy is to include P if it belongs to a projection * annotation that overlaps with the widget selection. * * D) P.widget_offset lies at S.widget_offset: Arguable - our policy * is to include P if it belongs to a projection annotation that * overlaps with the widget selection */ IRegion modelSelection= widgetRange2ModelRange(new Region(widgetSelection.x, widgetSelection.y)); if (modelSelection == null) return null; int modelOffset= modelSelection.getOffset(); int modelEndOffset= modelOffset + modelSelection.getLength(); /* Case A: never expand a zero-length selection. S is *behind* P. */ if (widgetSelection.y == 0) return new Point(modelEndOffset, 0); int widgetSelectionExclusiveEnd= widgetSelection.x + widgetSelection.y; Position[] annotationPositions= computeOverlappingAnnotationPositions(modelSelection); for (int i= 0; i < annotationPositions.length; i++) { IRegion[] regions= computeCollapsedRegions(annotationPositions[i]); if (regions == null) continue; for (int j= 0; j < regions.length; j++) { IRegion modelRange= regions[j]; IRegion widgetRange= modelRange2ClosestWidgetRange(modelRange); // only take collapsed ranges, i.e. widget length is 0 if (widgetRange != null && widgetRange.getLength() == 0) { int widgetOffset= widgetRange.getOffset(); // D) region is collapsed at S.widget_offset if (widgetOffset == widgetSelection.x) modelOffset= Math.min(modelOffset, modelRange.getOffset()); // C) region is collapsed at S.widget_end else if (widgetOffset == widgetSelectionExclusiveEnd) modelEndOffset= Math.max(modelEndOffset, modelRange.getOffset() + modelRange.getLength()); } } } return new Point(modelOffset, modelEndOffset - modelOffset); } /** * Returns the positions of all annotations that intersect with * modelSelection and that are at least partly visible. * @param modelSelection a model range * @return the positions of all annotations that intersect with * modelSelection * @since 3.1 */ private Position[] computeOverlappingAnnotationPositions(IRegion modelSelection) { List positions= new ArrayList<>(); for (Iterator e= fProjectionAnnotationModel.getAnnotationIterator(); e.hasNext();) { ProjectionAnnotation annotation= (ProjectionAnnotation) e.next(); Position position= fProjectionAnnotationModel.getPosition(annotation); if (position != null && position.overlapsWith(modelSelection.getOffset(), modelSelection.getLength()) && modelRange2WidgetRange(position) != null) positions.add(position); } return positions.toArray(new Position[positions.size()]); } @Override protected FindReplaceDocumentAdapter getFindReplaceDocumentAdapter() { if (fFindReplaceDocumentAdapter == null) { IDocument document= isProjectionMode() ? getDocument() : getVisibleDocument(); fFindReplaceDocumentAdapter= new FindReplaceDocumentAdapter(document); } return fFindReplaceDocumentAdapter; } @Override protected int findAndSelect(int startPosition, String findString, boolean forwardSearch, boolean caseSensitive, boolean wholeWord, boolean regExSearch) { if (!isProjectionMode()) return super.findAndSelect(startPosition, findString, forwardSearch, caseSensitive, wholeWord, regExSearch); StyledText textWidget= getTextWidget(); if (textWidget == null) return -1; try { IRegion matchRegion= getFindReplaceDocumentAdapter().find(startPosition, findString, forwardSearch, caseSensitive, wholeWord, regExSearch); if (matchRegion != null) { exposeModelRange(matchRegion); revealRange(matchRegion.getOffset(), matchRegion.getLength()); setSelectedRange(matchRegion.getOffset(), matchRegion.getLength()); return matchRegion.getOffset(); } } catch (BadLocationException x) { } return -1; } @Override protected int findAndSelectInRange(int startPosition, String findString, boolean forwardSearch, boolean caseSensitive, boolean wholeWord, int rangeOffset, int rangeLength, boolean regExSearch) { if (!isProjectionMode()) return super.findAndSelectInRange(startPosition, findString, forwardSearch, caseSensitive, wholeWord, rangeOffset, rangeLength, regExSearch); StyledText textWidget= getTextWidget(); if (textWidget == null) return -1; try { int modelOffset= startPosition; if (forwardSearch && (startPosition == -1 || startPosition < rangeOffset)) { modelOffset= rangeOffset; } else if (!forwardSearch && (startPosition == -1 || startPosition > rangeOffset + rangeLength)) { modelOffset= rangeOffset + rangeLength; } IRegion matchRegion= getFindReplaceDocumentAdapter().find(modelOffset, findString, forwardSearch, caseSensitive, wholeWord, regExSearch); if (matchRegion != null) { int offset= matchRegion.getOffset(); int length= matchRegion.getLength(); if (rangeOffset <= offset && offset + length <= rangeOffset + rangeLength) { exposeModelRange(matchRegion); revealRange(offset, length); setSelectedRange(offset, length); return offset; } } } catch (BadLocationException x) { } return -1; } }