/*******************************************************************************
 * 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 <anton.leherbauer@windriver.com> - [projection] Eclipse is too eager to unfold code - http://bugs.eclipse.org/178203
 *     Pawel Pogorzelski <pawel.pogorzelski1@gmail.com> - [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.
 * <p>
 * A projection source viewer uses a <code>ProjectionDocumentManager</code>
 * for the management of the visible document.</p>
 * <p>
 * NOTE: The <code>ProjectionViewer</code> only supports projections that cover full lines.
 * </p>
 * <p>
 * This class should not be subclassed.</p>
 *
 * @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<ProjectionCommand> fList= new ArrayList<>(15);
		int fExpectedExecutionCosts= -1;


		void add(ProjectionCommand command) {
			fList.add(command);
		}

		Iterator<ProjectionCommand> 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<ProjectionCommand> 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<IProjectionListener> fProjectionListeners;
	/** Internal lock for protecting the list of pending requests */
	private Object fLock= new Object();
	/** The list of pending requests */
	private List<AnnotationModelEvent> fPendingRequests= new ArrayList<>();
	/** The replace-visible-document execution trigger */
	private IDocument fReplaceVisibleDocumentExecutionTrigger;
	/** <code>true</code> 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 <code>true</code> 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 <code>null</code> 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 <code>null</code>. 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
	 * <code>null</code>. 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.isEmpty())
				fProjectionListeners= null;
		}
	}

	/**
	 * Notifies all registered projection listeners
	 * that projection mode has been enabled.
	 */
	protected void fireProjectionEnabled() {
		if (fProjectionListeners != null) {
			Iterator<IProjectionListener> 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<IProjectionListener> e= new ArrayList<>(fProjectionListeners).iterator();
			while (e.hasNext()) {
				IProjectionListener l= e.next();
				l.projectionDisabled();
			}
		}
	}

	/**
	 * Returns whether this viewer is in projection mode.
	 *
	 * @return <code>true</code> if this viewer is in projection mode,
	 *         <code>false</code> 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<Annotation> 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<Annotation> 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 &lt;= <code>offset</code>. If <code>testLastLine</code>
	 * is <code>true</code> and the offset is on last line then <code>offset</code> is returned.
	 *
	 * @param document the document
	 * @param offset the master document offset
	 * @param testLastLine <code>true</code> if the test for the last line should be performed
	 * @return the closest line offset &gt;= <code>offset</code>
	 * @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 <code>true</code> if a redraw request should be issued, <code>false</code> 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 <code>true</code> if a redraw request should be issued,
	 *        <code>false</code> 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 (ProjectionAnnotation c : collapsed) {
					IRegion[] regions = computeCollapsedRegions(fProjectionAnnotationModel.getPosition(c));
					if (regions != null) {
						for (IRegion region : regions) {
							removeMasterDocumentRange(projection, region.getOffset(), region.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(x);
				}

			} 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.isEmpty())
											return;
										ame= fPendingRequests.remove(0);
									}
									catchupWithProjectionAnnotationModel(ame);
								}
							} catch (BadLocationException x) {
								try {
									catchupWithProjectionAnnotationModel(null);
								} catch (BadLocationException x1) {
									throw new IllegalArgumentException(x1);
								} finally {
									synchronized (fLock) {
										fPendingRequests.clear();
									}
								}
							}
						});
					}
				}
			}
		}
	}

	/**
	 * Tests whether the visible document's master document
	 * is identical to this viewer's document.
	 *
	 * @return <code>true</code> 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 <code>null</code>,
	 * this is identical to a world change event.
	 *
	 * @param event the annotation model event or <code>null</code>
	 * @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<Position> 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<ProjectionCommand> 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<ProjectionAnnotation> annotations= new ArrayList<>(5);
		Iterator<Annotation> 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.isEmpty()) {
			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 (Annotation removedAnnotation : removedAnnotations) {
			ProjectionAnnotation annotation = (ProjectionAnnotation) removedAnnotation;
			if (annotation.isCollapsed()) {
				Position expanded= event.getPositionOfRemovedAnnotation(annotation);
				if (expanded != null) {
					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 <code>null</code> 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<Position> coverage) throws BadLocationException {
		for (Annotation a : annotations) {
			ProjectionAnnotation annotation = (ProjectionAnnotation) a;
			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 (IRegion region : regions) {
							collapse(region.getOffset(), region.getLength(), fireRedraw);
						}
					}
				} else {
					expand(position.getOffset(), position.getLength(), fireRedraw);
				}
			}
		}
	}

	private boolean covers(List<Position> coverage, Position position) {
		Iterator<Position> 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<Annotation> 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 (IRegion region : regions) {
								removeMasterDocumentRange(projection, region.getOffset(), region.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<ProjectionAnnotation> expand= new ArrayList<>(2);
			// expand the immediate affected collapsed regions
			Iterator<Annotation> 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<ProjectionAnnotation> 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 (Position annotationPosition : annotationPositions) {
			IRegion[] regions = computeCollapsedRegions(annotationPosition);
			if (regions == null)
				continue;
			for (IRegion modelRange : regions) {
				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
	 * <code>modelSelection</code> and that are at least partly visible.
	 * @param modelSelection a model range
	 * @return the positions of all annotations that intersect with
	 *         <code>modelSelection</code>
	 * @since 3.1
	 */
	private Position[] computeOverlappingAnnotationPositions(IRegion modelSelection) {
		List<Position> positions= new ArrayList<>();
		for (Iterator<Annotation> 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;
	}
}
